77import DatadogInternal
88import Foundation
99
10- internal class RUMAppLaunchVitalScope {
10+ internal class RUMAppLaunchManager {
1111 internal enum Constants {
1212 // Maximum time for an erroneous TTID (Time to Initial Display)
1313 static let maxTTIDDuration : TimeInterval = 60 // 1 minute
14+ // Maximum time for an erroneous ttfd
15+ static let maxTTFDDuration : TimeInterval = 90 // 90 seconds
1416 }
1517 // MARK: - Properties
1618
1719 private unowned let parent : RUMContextProvider
1820 private let dependencies : RUMScopeDependencies
1921
2022 private var timeToInitialDisplay : Double ?
23+ private var timeToFullDisplay : Double ?
24+ private var viewLoadingTime : Double ?
25+ private var startupType : RUMVitalEvent . Vital . AppLaunchProperties . StartupType ?
2126
2227 private lazy var startupTypeHandler = StartupTypeHandler ( appStateManager: dependencies. appStateManager)
2328
@@ -35,84 +40,79 @@ internal class RUMAppLaunchVitalScope {
3540 switch command {
3641 case let command as RUMTimeToInitialDisplayCommand :
3742 try writeTTIDVitalEvent ( from: command, context: context, writer: writer)
43+ case let command as RUMTimeToFullDisplayCommand :
44+ try writeTTFDVitalEvent ( from: command, context: context, writer: writer)
45+ case let command as RUMAddViewLoadingTime :
46+ try registerViewLoadingTime ( from: command, context: context)
47+ case let command as RUMStopViewCommand :
48+ if viewLoadingTime != nil ,
49+ timeToFullDisplay == nil {
50+ try writeTTFDVitalEvent ( from: command, context: context, writer: writer)
51+ }
3852 default : break
3953 }
4054 } catch {
4155 dependencies. telemetry. error ( " RUMAppLaunchManager failed to write the ttid vital event. " , error: error)
4256 }
4357 }
58+ }
4459
45- // MARK: - Private Methods
60+ // MARK: - TTID
4661
62+ private extension RUMAppLaunchManager {
4763 private func writeTTIDVitalEvent( from command: RUMTimeToInitialDisplayCommand , context: DatadogContext , writer: Writer ) throws {
48- guard
49- shouldProcess ( command: command, context: context) ,
50- let timeToInitialDisplay = timeToInitialDisplay ( from: command, context: context)
64+ guard shouldProcess ( command: command, context: context) ,
65+ let ttid = time ( from: command, context: context)
5166 else {
5267 return
5368 }
5469
55- self . timeToInitialDisplay = timeToInitialDisplay
56-
57- let attributes = command. globalAttributes
58- . merging ( command. attributes) { $1 }
70+ self . timeToInitialDisplay = ttid
5971
6072 dependencies. appStateManager. currentAppStateInfo { [ weak self] currentAppStateInfo in
6173 guard let self else {
6274 return
6375 }
6476
65- let appLaunchMetric : RUMVitalEvent . Vital . AppLaunchProperties . AppLaunchMetric = . ttid
77+ let attributes = command. globalAttributes
78+ . merging ( command. attributes) { $1 }
79+
6680 let startupType = self . startupTypeHandler. startupType ( currentAppState: currentAppStateInfo)
67- let vital = RUMVitalEvent . Vital. appLaunchProperties (
68- value: RUMVitalEvent . Vital. AppLaunchProperties (
69- appLaunchMetric: . ttid,
70- duration: Double ( timeToInitialDisplay. toInt64Nanoseconds) ,
71- id: dependencies. rumUUIDGenerator. generateUnique ( ) . toRUMDataFormat,
72- isPrewarmed: context. launchInfo. launchReason == . prewarming,
73- name: appLaunchMetric. name,
74- startupType: startupType
75- )
76- )
81+ self . startupType = startupType
7782
78- let vitalEvent = RUMVitalEvent (
79- dd: . init( ) ,
80- account: . init( context: context) ,
81- application: . init( id: parent. context. rumApplicationID) ,
82- buildId: context. buildId,
83- buildVersion: context. buildNumber,
84- ciTest: dependencies. ciTest,
85- connectivity: . init( context: context) ,
86- context: RUMEventAttributes ( contextInfo: attributes) ,
87- date: context. launchInfo. processLaunchDate. timeIntervalSince1970. toInt64Milliseconds,
88- ddtags: context. ddTags,
89- device: context. normalizedDevice ( ) ,
90- os: context. os,
91- service: context. service,
92- session: . init(
93- hasReplay: context. hasReplay,
94- id: parent. context. sessionID. toRUMDataFormat,
95- type: dependencies. sessionType
96- ) ,
97- source: . init( rawValue: context. source) ?? . ios,
98- synthetics: dependencies. syntheticsTest,
99- usr: . init( context: context) ,
100- version: context. version,
101- vital: vital
83+ self . writeVitalEvent (
84+ duration: Double ( ttid. toInt64Nanoseconds) ,
85+ appLaunchMetric: . ttid,
86+ startupType: startupType,
87+ attributes: attributes,
88+ context: context,
89+ writer: writer
10290 )
10391
104- writer. write ( value: vitalEvent)
92+ // The TTFD is always written after the TTID. If it exists already, means it was not written before.
93+ if let timeToFullDisplay {
94+ let ttfd = max ( ttid, timeToFullDisplay)
95+ self . writeVitalEvent (
96+ duration: Double ( ttfd. toInt64Nanoseconds) ,
97+ appLaunchMetric: . ttfd,
98+ startupType: startupType,
99+ attributes: attributes,
100+ context: context,
101+ writer: writer
102+ )
103+ }
105104 }
106105 }
107106
108- private func shouldProcess( command: RUMTimeToInitialDisplayCommand , context: DatadogContext ) -> Bool {
107+ func shouldProcess( command: RUMTimeToInitialDisplayCommand , context: DatadogContext ) -> Bool {
109108 // Ignore command if the time to initial display was already written
110109 guard self . timeToInitialDisplay == nil else {
111110 return false
112111 }
113112
114- // Ignore command if the time to initial display is too big
115- guard command. time. timeIntervalSince ( context. launchInfo. processLaunchDate) < Constants . maxTTIDDuration else {
113+ // Ignore command if the time since the SDK load is too big
114+ guard let runtimeLoadDate = context. launchInfo. launchPhaseDates [ . runtimeLoad] ,
115+ command. time. timeIntervalSince ( runtimeLoadDate) < Constants . maxTTIDDuration else {
116116 return false
117117 }
118118
@@ -124,7 +124,7 @@ internal class RUMAppLaunchVitalScope {
124124 return true
125125 }
126126
127- private func timeToInitialDisplay ( from command: RUMTimeToInitialDisplayCommand , context: DatadogContext ) -> TimeInterval ? {
127+ func time ( from command: RUMCommand , context: DatadogContext ) -> TimeInterval ? {
128128 switch context. launchInfo. launchReason {
129129 case . userLaunch:
130130 return command. time. timeIntervalSince ( context. launchInfo. processLaunchDate)
@@ -138,6 +138,138 @@ internal class RUMAppLaunchVitalScope {
138138 return nil
139139 }
140140 }
141+
142+ func writeVitalEvent(
143+ duration: TimeInterval ,
144+ appLaunchMetric: RUMVitalEvent . Vital . AppLaunchProperties . AppLaunchMetric ,
145+ startupType: RUMVitalEvent . Vital . AppLaunchProperties . StartupType ,
146+ attributes: [ AttributeKey : AttributeValue ] ,
147+ context: DatadogContext ,
148+ writer: Writer
149+ ) {
150+ let vital = RUMVitalEvent . Vital. appLaunchProperties (
151+ value: RUMVitalEvent . Vital. AppLaunchProperties (
152+ appLaunchMetric: appLaunchMetric,
153+ duration: duration,
154+ id: dependencies. rumUUIDGenerator. generateUnique ( ) . toRUMDataFormat,
155+ isPrewarmed: context. launchInfo. launchReason == . prewarming,
156+ name: appLaunchMetric. name,
157+ startupType: startupType
158+ )
159+ )
160+
161+ let vitalEvent = RUMVitalEvent (
162+ dd: . init( ) ,
163+ account: . init( context: context) ,
164+ application: . init( id: parent. context. rumApplicationID) ,
165+ buildId: context. buildId,
166+ buildVersion: context. buildNumber,
167+ ciTest: dependencies. ciTest,
168+ connectivity: . init( context: context) ,
169+ context: RUMEventAttributes ( contextInfo: attributes) ,
170+ date: context. launchInfo. processLaunchDate. timeIntervalSince1970. toInt64Milliseconds,
171+ ddtags: context. ddTags,
172+ device: context. normalizedDevice ( ) ,
173+ os: context. os,
174+ service: context. service,
175+ session: . init(
176+ hasReplay: context. hasReplay,
177+ id: parent. context. sessionID. toRUMDataFormat,
178+ type: dependencies. sessionType
179+ ) ,
180+ source: . init( rawValue: context. source) ?? . ios,
181+ synthetics: dependencies. syntheticsTest,
182+ usr: . init( context: context) ,
183+ version: context. version,
184+ vital: vital
185+ )
186+
187+ writer. write ( value: vitalEvent)
188+ }
189+ }
190+
191+ // MARK: - TTFD
192+
193+ private extension RUMAppLaunchManager {
194+ func writeTTFDVitalEvent( from command: RUMTimeToFullDisplayCommand , context: DatadogContext , writer: Writer ) throws {
195+ guard shouldProcess ( command: command, context: context) ,
196+ let ttfd = time ( from: command, context: context) else { return }
197+
198+ self . timeToFullDisplay = ttfd
199+
200+ if let timeToFullDisplay, let timeToInitialDisplay, let startupType {
201+ let attributes = command. globalAttributes
202+ . merging ( command. attributes) { $1 }
203+ let ttfd = max ( timeToInitialDisplay, timeToFullDisplay)
204+
205+ self . writeVitalEvent (
206+ duration: Double ( ttfd. toInt64Nanoseconds) ,
207+ appLaunchMetric: . ttfd,
208+ startupType: startupType,
209+ attributes: attributes,
210+ context: context,
211+ writer: writer
212+ )
213+ }
214+ }
215+
216+ func shouldProcess( command: RUMTimeToFullDisplayCommand , context: DatadogContext ) -> Bool {
217+ // Ignore command if the time to full display was already written
218+ guard self . timeToFullDisplay == nil else {
219+ return false
220+ }
221+
222+ // Ignore command if the time since the SDK load is too big
223+ guard let runtimeLoadDate = context. launchInfo. launchPhaseDates [ . runtimeLoad] ,
224+ command. time. timeIntervalSince ( runtimeLoadDate) < Constants . maxTTFDDuration else {
225+ return false
226+ }
227+
228+ return true
229+ }
230+ }
231+
232+ // MARK: - View loading time
233+
234+ private extension RUMAppLaunchManager {
235+ func registerViewLoadingTime( from command: RUMAddViewLoadingTime , context: DatadogContext ) throws {
236+ guard shouldProcess ( command: command, context: context) ,
237+ let viewLoadingTime = time ( from: command, context: context) else { return }
238+
239+ self . viewLoadingTime = viewLoadingTime
240+ }
241+
242+ func writeTTFDVitalEvent( from command: RUMStopViewCommand , context: DatadogContext , writer: Writer ) throws {
243+ if let viewLoadingTime, let timeToInitialDisplay, let startupType {
244+ let attributes = command. globalAttributes
245+ . merging ( command. attributes) { $1 }
246+ let ttfd = max ( timeToInitialDisplay, viewLoadingTime)
247+
248+ self . writeVitalEvent (
249+ duration: Double ( ttfd. toInt64Nanoseconds) ,
250+ appLaunchMetric: . ttfd,
251+ startupType: startupType,
252+ attributes: attributes,
253+ context: context,
254+ writer: writer
255+ )
256+ }
257+ }
258+
259+ func shouldProcess( command: RUMAddViewLoadingTime , context: DatadogContext ) -> Bool {
260+ // Ignore command if the time to full display was already written
261+ guard self . timeToFullDisplay == nil else {
262+ return false
263+ }
264+
265+ // Ignore command if the time since the SDK load is too big
266+ guard let runtimeLoadDate = context. launchInfo. launchPhaseDates [ . runtimeLoad] ,
267+ command. time. timeIntervalSince ( runtimeLoadDate) < Constants . maxTTFDDuration else {
268+ return false
269+ }
270+
271+ return true
272+ }
141273}
142274
143275private extension RUMVitalEvent . Vital . AppLaunchProperties . AppLaunchMetric {
0 commit comments