diff --git a/Cargo.Bazel.lock b/Cargo.Bazel.lock index 50c67bf5d..d4688f39e 100644 --- a/Cargo.Bazel.lock +++ b/Cargo.Bazel.lock @@ -1,5 +1,5 @@ { - "checksum": "56425165453d96ae5bf70ff6018fcb7c04df849e34a7e81588062e458fd2a43b", + "checksum": "8ad31ce9dea7ee209cf012275ebd9379bd6f5ccc1ba1f90281d98b110fbb673f", "crates": { "addr2line 0.22.0": { "name": "addr2line", @@ -11376,6 +11376,10 @@ "id": "bd-session 1.0.0", "target": "bd_session" }, + { + "id": "log 0.4.22", + "target": "log" + }, { "id": "parking_lot 0.12.3", "target": "parking_lot" diff --git a/Cargo.lock b/Cargo.lock index 80dab9601..99e63c72a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1912,6 +1912,7 @@ dependencies = [ "bd-logger", "bd-runtime", "bd-session", + "log", "parking_lot", "pretty_assertions", "regex", diff --git a/examples/android/MainActivity.kt b/examples/android/MainActivity.kt index 2e405376b..1d25d77ab 100644 --- a/examples/android/MainActivity.kt +++ b/examples/android/MainActivity.kt @@ -48,6 +48,9 @@ import okhttp3.Request import okhttp3.Response import java.io.IOException import kotlin.system.exitProcess +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration class MainActivity : ComponentActivity() { @@ -121,6 +124,8 @@ class MainActivity : ComponentActivity() { Logger.addField("field_container_field_key", "field_container_field_value") Logger.logInfo(mapOf("key" to "value")) { "MainActivity onCreate called" } + + Logger.logAppLaunchTTI(1.toDuration(DurationUnit.SECONDS)) } private fun createComposeUI() { diff --git a/examples/swift/hello_world/ContentView.swift b/examples/swift/hello_world/ContentView.swift index ff63e1409..edd52ef3f 100644 --- a/examples/swift/hello_world/ContentView.swift +++ b/examples/swift/hello_world/ContentView.swift @@ -17,8 +17,8 @@ struct ContentView: View { @State private var selectedLogLevel = LoggerCustomer.LogLevel.info init(navigationController: UINavigationController?) { - self.navigationController = navigationController self.loggerCustomer = LoggerCustomer() + self.navigationController = navigationController self.currentSessionID = self.loggerCustomer.sessionID ?? "No Session ID" } @@ -103,6 +103,7 @@ struct ContentView: View { } } .padding(5) + .onAppear { self.loggerCustomer.logAppLaunchTTI() } } } diff --git a/examples/swift/hello_world/LoggerCustomer.swift b/examples/swift/hello_world/LoggerCustomer.swift index d2322f84a..22e48e41f 100644 --- a/examples/swift/hello_world/LoggerCustomer.swift +++ b/examples/swift/hello_world/LoggerCustomer.swift @@ -44,6 +44,8 @@ final class LoggerCustomer: NSObject, URLSessionDelegate { var id: String { return self.rawValue } } + private var appStartTime: Date + private let requestDefinitions = [ // swiftlint:disable:next force_unwrapping use_static_string_url_init RequestDefinition(method: "GET", url: URL(string: "https://httpbin.org/get")!), @@ -79,6 +81,8 @@ final class LoggerCustomer: NSObject, URLSessionDelegate { } override init() { + self.appStartTime = Date() + Logger .configure( withAPIKey: kBitdriftAPIKey, @@ -160,6 +164,10 @@ final class LoggerCustomer: NSObject, URLSessionDelegate { Logger.logTrace("Sending log with level [Trace]", fields: fields) } } + + func logAppLaunchTTI() { + Logger.logAppLaunchTTI(Date().timeIntervalSince(self.appStartTime)) + } } extension LoggerCustomer: MXMetricManagerSubscriber { diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt index 6db5116e9..4032db39a 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt @@ -20,6 +20,7 @@ import io.bitdrift.capture.providers.SystemDateProvider import io.bitdrift.capture.providers.session.SessionStrategy import okhttp3.HttpUrl import java.util.concurrent.atomic.AtomicReference +import kotlin.time.Duration /** * Top level namespace Capture SDK. @@ -278,6 +279,18 @@ object Capture { logger()?.log(level = level, fields = fields, throwable = throwable, message = message) } + /** + * Writes an app launch TTI log event. This event should be logged only once per Logger configuration. + * Consecutive calls have no effect. + * + * @param duration The time between a user's intent to launch the app and when the app becomes + * interactive. Calls with a negative duration are ignored. + */ + @JvmStatic + fun logAppLaunchTTI(duration: Duration) { + logger()?.logAppLaunchTTI(duration) + } + /** * Signals that an operation has started at this point in time. * diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/CaptureJniLibrary.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/CaptureJniLibrary.kt index 912c0e160..6e51bf73a 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/CaptureJniLibrary.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/CaptureJniLibrary.kt @@ -219,7 +219,7 @@ internal object CaptureJniLibrary { * @param appVersion * @param appVersionCode * @param appInstallSizeBytes the size of the app installation. Expressed in bytes. - * @param durationS the duration of time the preparation of the log took. + * @param durationS the duration of time the preparation of the log took. Expressed in seconds. */ external fun writeAppUpdateLog( loggerId: Long, @@ -229,6 +229,19 @@ internal object CaptureJniLibrary { durationS: Double, ) + /** + * Writes an app launch TTI log. The method should be called only once per logger Id. Consecutive calls + * have no effect. + * + * @param loggerId the ID of the logger to write to. + * @param durationS the time between a user's intent to launch the app and when the app becomes + * interactive. Expressed in seconds. Calls with a negative duration are ignored. + */ + external fun writeAppLaunchTTILog( + loggerId: Long, + durationS: Double, + ) + /** * Flushes logger's state to disk. * diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/ILogger.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/ILogger.kt index 01d81cff4..9d1c98edc 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/ILogger.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/ILogger.kt @@ -10,6 +10,7 @@ package io.bitdrift.capture import io.bitdrift.capture.events.span.Span import io.bitdrift.capture.network.HttpRequestInfo import io.bitdrift.capture.network.HttpResponseInfo +import kotlin.time.Duration /** * A Capture SDK logger interface. @@ -72,6 +73,7 @@ interface ILogger { /** * Logs a message at a specified level. + * * @param level the severity of the log. * @param fields and optional collection of key-value pairs to be added to the log line. * @param throwable an optional throwable to include in the log line. @@ -84,6 +86,15 @@ interface ILogger { message: () -> String, ) + /** + * Writes an app launch TTI log event. This event should be logged only once per Logger configuration. + * Consecutive calls have no effect. + * + * @param duration The time between a user's intent to launch the app and when the app becomes + * interactive. Calls with a negative duration are ignored. + */ + fun logAppLaunchTTI(duration: Duration) + /** * Signals that an operation has started at this point in time. Each operation consists of start * and end event logs. The start event is emitted immediately upon calling the @@ -100,12 +111,14 @@ interface ILogger { /** * Records information about an HTTP network request + * * @param httpRequestInfo information used to enrich the log line */ fun log(httpRequestInfo: HttpRequestInfo) /** * Records information about an HTTP network response + * * @param httpResponseInfo information used to enrich the log line */ fun log(httpResponseInfo: HttpResponseInfo) diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt index fc2dbd4eb..6c34120b6 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt @@ -284,6 +284,10 @@ internal class LoggerImpl( ) } + override fun logAppLaunchTTI(duration: Duration) { + CaptureJniLibrary.writeAppLaunchTTILog(this.loggerId, duration.toDouble(DurationUnit.SECONDS)) + } + override fun startSpan(name: String, level: LogLevel, fields: Map?): Span { return Span(this, name, level, fields) } diff --git a/platform/jvm/jni_symbols.lds b/platform/jvm/jni_symbols.lds index b8a376ad9..8c51ce4d7 100644 --- a/platform/jvm/jni_symbols.lds +++ b/platform/jvm/jni_symbols.lds @@ -15,6 +15,7 @@ Java_io_bitdrift_capture_CaptureJniLibrary_writeResourceUtilizationLog Java_io_bitdrift_capture_CaptureJniLibrary_writeSDKConfiguredLog Java_io_bitdrift_capture_CaptureJniLibrary_shouldWriteAppUpdateLog Java_io_bitdrift_capture_CaptureJniLibrary_writeAppUpdateLog +Java_io_bitdrift_capture_CaptureJniLibrary_writeAppLaunchTTILog Java_io_bitdrift_capture_CaptureJniLibrary_flush Java_io_bitdrift_capture_CaptureJniLibrary_debugDebug Java_io_bitdrift_capture_CaptureJniLibrary_debugError diff --git a/platform/jvm/src/jni.rs b/platform/jvm/src/jni.rs index 1b2032dd9..b8c0e18ec 100644 --- a/platform/jvm/src/jni.rs +++ b/platform/jvm/src/jni.rs @@ -995,6 +995,25 @@ pub extern "system" fn Java_io_bitdrift_capture_CaptureJniLibrary_writeAppUpdate ); } +#[no_mangle] +pub extern "system" fn Java_io_bitdrift_capture_CaptureJniLibrary_writeAppLaunchTTILog( + _env: JNIEnv<'_>, + _class: JClass<'_>, + logger_id: jlong, + duration_s: f64, +) { + bd_client_common::error::with_handle_unexpected( + || -> anyhow::Result<()> { + let logger = unsafe { LoggerId::from_raw(logger_id) }; + logger.log_app_launch_tti(Duration::seconds_f64(duration_s)); + + Ok(()) + }, + "jni write app launch TTI log", + ); +} + + #[no_mangle] pub extern "system" fn Java_io_bitdrift_capture_CaptureJniLibrary_flush( _env: JNIEnv<'_>, diff --git a/platform/shared/Cargo.toml b/platform/shared/Cargo.toml index b5104d8fb..1fab6c5ed 100644 --- a/platform/shared/Cargo.toml +++ b/platform/shared/Cargo.toml @@ -14,6 +14,7 @@ bd-log-primitives.workspace = true bd-logger.workspace = true bd-runtime.workspace = true bd-session.workspace = true +log.workspace = true parking_lot.workspace = true regex.workspace = true time.workspace = true diff --git a/platform/shared/src/lib.rs b/platform/shared/src/lib.rs index 2d67eb18b..c8c5f66d2 100644 --- a/platform/shared/src/lib.rs +++ b/platform/shared/src/lib.rs @@ -9,7 +9,9 @@ pub mod error; pub mod metadata; use bd_client_common::error::handle_unexpected; +use bd_logger::{log_level, AnnotatedLogField, LogField, LogFieldKind, LogType}; use bd_runtime::runtime::Snapshot; +use parking_lot::Once; use std::future::Future; use std::ops::Deref; use std::pin::Pin; @@ -92,6 +94,7 @@ pub struct LoggerHolder { logger: bd_logger::Logger, handle: bd_logger::LoggerHandle, future: parking_lot::Mutex>, + app_launch_tti_log: Once, } impl Deref for LoggerHolder { @@ -109,6 +112,7 @@ impl LoggerHolder { logger, handle, future: parking_lot::Mutex::new(Some(future)), + app_launch_tti_log: Once::new(), } } @@ -158,6 +162,39 @@ impl LoggerHolder { holder.shutdown(false); drop(holder); } + + /// Logs an out-of-the-box app launch TTI log event. The method should be called only once. + /// Consecutive calls have not effect. + pub fn log_app_launch_tti(&self, duration: time::Duration) { + self.app_launch_tti_log.call_once(|| { + let duration_ms = duration.as_seconds_f64() * 1_000f64; + if duration_ms < 0.0 { + log::warn!( + "dropping app launch TTI log: reported TTI is negative: {}", + duration_ms + ); + return; + } + + let fields = vec![AnnotatedLogField { + field: LogField { + key: "_duration_ms".into(), + value: duration_ms.to_string().into(), + }, + kind: LogFieldKind::Ootb, + }]; + + self.log( + log_level::INFO, + LogType::Lifecycle, + "AppLaunchTTI".into(), + fields, + vec![], + None, + false, + ); + }); + } } impl<'a> From> for i64 { diff --git a/platform/swift/source/Capture.swift b/platform/swift/source/Capture.swift index ad0b4b37f..f2fe5b55c 100644 --- a/platform/swift/source/Capture.swift +++ b/platform/swift/source/Capture.swift @@ -261,6 +261,17 @@ extension Logger { ) } + // MARK: - Predefined Logs + + /// Writes an app launch TTI log event. This event should be logged only once per Logger configuration. + /// Consecutive calls have no effect. + /// + /// - parameter duration: The time between a user's intent to launch the app and when the app becomes + /// interactive. Calls with a negative duration are ignored. + public static func logAppLaunchTTI(_ duration: TimeInterval) { + Self.getShared()?.logAppLaunchTTI(duration) + } + // MARK: - Network Activity Logging /// Logs information about a network request. diff --git a/platform/swift/source/CaptureRustBridge.h b/platform/swift/source/CaptureRustBridge.h index 019ca8557..4af16cb17 100644 --- a/platform/swift/source/CaptureRustBridge.h +++ b/platform/swift/source/CaptureRustBridge.h @@ -137,14 +137,27 @@ bool capture_should_write_app_update_log( * @param app_version the version of the app. * @param build_number the app build number. * @param app_install_size_bytes the size of the app in bytes. - * @param duration_ms the duration of time the preparation of the log took. + * @param duration_s the duration of time the preparation of the log took. */ void capture_write_app_update_log( logger_id logger_id, NSString *app_version, NSString *build_number, uint64_t app_install_size_bytes, - double duration_ms + double duration_s +); + +/* + * Writes an app launch TTI log. The method should be called only once per logger Id. Consecutive calls + * have no effect. + * + * @param loggerId the ID of the logger to write to. + * @param duration_s the duration of time between a user's intent to launch an app and the point in time + * when the app became interactive. + */ +void capture_write_app_launch_tti_log( + logger_id logger_id, + double duration_s ); /* diff --git a/platform/swift/source/CoreLogger.swift b/platform/swift/source/CoreLogger.swift index b1ffbcaab..e88d63129 100644 --- a/platform/swift/source/CoreLogger.swift +++ b/platform/swift/source/CoreLogger.swift @@ -120,6 +120,10 @@ extension CoreLogger: CoreLogging { ) } + func logAppLaunchTTI(_ duration: TimeInterval) { + self.underlyingLogger.logAppLaunchTTI(duration) + } + func startNewSession() { self.underlyingLogger.startNewSession() } diff --git a/platform/swift/source/CoreLogging.swift b/platform/swift/source/CoreLogging.swift index 643dd107f..93197624d 100644 --- a/platform/swift/source/CoreLogging.swift +++ b/platform/swift/source/CoreLogging.swift @@ -89,6 +89,13 @@ protocol CoreLogging: AnyObject { duration: TimeInterval ) + /// Writes an app launch TTI log event. This event should be logged only once per Logger configuration. + /// Consecutive calls have no effect. + /// + /// - parameter duration: The time between a user's intent to launch the app and when the app becomes + /// interactive. + func logAppLaunchTTI(_ duration: TimeInterval) + /// Stars new session using configured session strategy. func startNewSession() diff --git a/platform/swift/source/Logger.swift b/platform/swift/source/Logger.swift index 703a7344b..ab0328fb3 100644 --- a/platform/swift/source/Logger.swift +++ b/platform/swift/source/Logger.swift @@ -442,6 +442,10 @@ extension Logger: Logging { self.deviceCodeController.createTemporaryDeviceCode(deviceID: self.deviceID, completion: completion) } + public func logAppLaunchTTI(_ duration: TimeInterval) { + self.underlyingLogger.logAppLaunchTTI(duration) + } + public func startSpan( name: String, level: LogLevel, diff --git a/platform/swift/source/LoggerBridge.swift b/platform/swift/source/LoggerBridge.swift index 3851f8be9..41d68166c 100644 --- a/platform/swift/source/LoggerBridge.swift +++ b/platform/swift/source/LoggerBridge.swift @@ -127,6 +127,10 @@ final class LoggerBridge: LoggerBridging { capture_write_app_update_log(self.loggerID, appVersion, buildNumber, appSizeBytes, duration) } + func logAppLaunchTTI(_ duration: TimeInterval) { + capture_write_app_launch_tti_log(self.loggerID, duration) + } + func startNewSession() { capture_start_new_session(self.loggerID) } diff --git a/platform/swift/source/LoggerBridging.swift b/platform/swift/source/LoggerBridging.swift index 1eea7ca3b..7cc236915 100644 --- a/platform/swift/source/LoggerBridging.swift +++ b/platform/swift/source/LoggerBridging.swift @@ -41,6 +41,8 @@ protocol LoggerBridging { duration: TimeInterval ) + func logAppLaunchTTI(_ duration: TimeInterval) + func start() func startNewSession() diff --git a/platform/swift/source/Logging.swift b/platform/swift/source/Logging.swift index be26bf510..a90cff52f 100644 --- a/platform/swift/source/Logging.swift +++ b/platform/swift/source/Logging.swift @@ -95,6 +95,17 @@ public protocol Logging { /// main queue. func createTemporaryDeviceCode(completion: @escaping (Result) -> Void) + // MARK: - Predefined logs + + /// Writes an app launch TTI log event. This event should be logged only once per Logger configuration. + /// Consecutive calls have no effect. + /// + /// - parameter duration: The time between a user's intent to launch the app and when the app becomes + /// interactive. Calls with a negative duration are ignored. + func logAppLaunchTTI(_ duration: TimeInterval) + + // MARK: - Spans + /// Signals that an operation has started at this point in time. Each operation consists of start and /// end event logs. The start event is emitted immediately upon calling the `startSpan(...)` method, /// while the corresponding end event is emitted when the `end(...)` method is called on the `Span` diff --git a/platform/swift/source/src/bridge.rs b/platform/swift/source/src/bridge.rs index ac8e68d3f..0de572675 100644 --- a/platform/swift/source/src/bridge.rs +++ b/platform/swift/source/src/bridge.rs @@ -738,6 +738,17 @@ extern "C" fn capture_write_app_update_log( ); } +#[no_mangle] +extern "C" fn capture_write_app_launch_tti_log(logger_id: LoggerId<'_>, duration_s: f64) { + with_handle_unexpected( + || -> anyhow::Result<()> { + logger_id.log_app_launch_tti(Duration::seconds_f64(duration_s)); + Ok(()) + }, + "swift write app launch TTI log", + ); +} + #[no_mangle] extern "C" fn capture_start_new_session(logger_id: LoggerId<'_>) { logger_id.start_new_session(); diff --git a/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift b/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift index 7bf2fca33..718371516 100644 --- a/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift +++ b/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift @@ -124,6 +124,8 @@ extension MockCoreLogging: CoreLogging { self.logAppUpdateExpectation?.fulfill() } + func logAppLaunchTTI(_: TimeInterval) {} + func addField(withKey _: String, value _: String) {} func removeField(withKey _: String) {} diff --git a/test/platform/swift/unit_integration/mocks/MockLoggerBridging.swift b/test/platform/swift/unit_integration/mocks/MockLoggerBridging.swift index 7609f8cf4..5fd833b12 100644 --- a/test/platform/swift/unit_integration/mocks/MockLoggerBridging.swift +++ b/test/platform/swift/unit_integration/mocks/MockLoggerBridging.swift @@ -94,6 +94,8 @@ extension MockLoggerBridging: LoggerBridging { self.logAppUpdateExpectation?.fulfill() } + func logAppLaunchTTI(_: TimeInterval) {} + func addField(withKey _: String, value _: String) {} func removeField(withKey _: String) {} diff --git a/test/platform/swift/unit_integration/mocks/MockLogging.swift b/test/platform/swift/unit_integration/mocks/MockLogging.swift index 386920d7e..b7db23649 100644 --- a/test/platform/swift/unit_integration/mocks/MockLogging.swift +++ b/test/platform/swift/unit_integration/mocks/MockLogging.swift @@ -109,6 +109,8 @@ extension MockLogging: Logging { self.onLog(log) } + func logAppLaunchTTI(_: TimeInterval) {} + func addField(withKey _: String, value _: FieldValue) {} func removeField(withKey _: String) {}