diff --git a/Cargo.Bazel.lock b/Cargo.Bazel.lock index 0afc31a4..bbc972bd 100644 --- a/Cargo.Bazel.lock +++ b/Cargo.Bazel.lock @@ -1,5 +1,5 @@ { - "checksum": "6dddb74cdf478e7a0d3088cb4ed522bb680ee9f7ab2029daeec43c1c4f83f9e2", + "checksum": "b7eab6bc462fb73cb8d884edebf0d9ff40e1c58fbf0a74fad5203c72a5c02c4a", "crates": { "addr2line 0.24.2": { "name": "addr2line", @@ -1337,7 +1337,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-api" } @@ -1474,7 +1474,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-buffer" } @@ -1603,7 +1603,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-client-common" } @@ -1707,7 +1707,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-client-stats" } @@ -1824,7 +1824,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-client-stats-store" } @@ -1896,7 +1896,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-completion" } @@ -1952,7 +1952,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-device" } @@ -2036,7 +2036,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-events" } @@ -2105,7 +2105,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-grpc" } @@ -2309,7 +2309,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-grpc-codec" } @@ -2392,7 +2392,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-hyper-network" } @@ -2508,7 +2508,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-internal-logging" } @@ -2572,7 +2572,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-key-value" } @@ -2648,7 +2648,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-log" } @@ -2732,7 +2732,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-log-filter" } @@ -2812,7 +2812,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-log-matcher" } @@ -2884,7 +2884,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-log-metadata" } @@ -2940,7 +2940,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-log-primitives" } @@ -2996,7 +2996,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-logger" } @@ -3114,6 +3114,10 @@ "id": "bd-session 1.0.0", "target": "bd_session" }, + { + "id": "bd-session-replay 1.0.0", + "target": "bd_session_replay" + }, { "id": "bd-shutdown 1.0.0", "target": "bd_shutdown" @@ -3209,7 +3213,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-matcher" } @@ -3277,7 +3281,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-metadata" } @@ -3353,7 +3357,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-network-quality" } @@ -3392,7 +3396,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-noop-network" } @@ -3457,7 +3461,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-pgv" } @@ -3552,7 +3556,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-proto" } @@ -3655,7 +3659,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-resource-utilization" } @@ -3736,7 +3740,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-runtime" } @@ -3808,7 +3812,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-server-stats" } @@ -3900,7 +3904,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-session" } @@ -3988,6 +3992,111 @@ "license_ids": [], "license_file": "../LICENSE" }, + "bd-session-replay 1.0.0": { + "name": "bd-session-replay", + "version": "1.0.0", + "package_url": null, + "repository": { + "Git": { + "remote": "https://github.com/bitdriftlabs/shared-core.git", + "commitish": { + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" + }, + "strip_prefix": "bd-session-replay" + } + }, + "targets": [ + { + "Library": { + "crate_name": "bd_session_replay", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "bd_session_replay", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "anyhow 1.0.90", + "target": "anyhow" + }, + { + "id": "bd-client-common 1.0.0", + "target": "bd_client_common" + }, + { + "id": "bd-client-stats-store 1.0.0", + "target": "bd_client_stats_store" + }, + { + "id": "bd-internal-logging 1.0.0", + "target": "bd_internal_logging" + }, + { + "id": "bd-log-primitives 1.0.0", + "target": "bd_log_primitives" + }, + { + "id": "bd-runtime 1.0.0", + "target": "bd_runtime" + }, + { + "id": "bd-shutdown 1.0.0", + "target": "bd_shutdown" + }, + { + "id": "bd-stats-common 1.0.0", + "target": "bd_stats_common" + }, + { + "id": "bd-time 1.0.0", + "target": "bd_time" + }, + { + "id": "log 0.4.22", + "target": "log" + }, + { + "id": "parking_lot 0.12.3", + "target": "parking_lot" + }, + { + "id": "time 0.3.36", + "target": "time" + }, + { + "id": "tokio 1.41.0", + "target": "tokio" + } + ], + "selects": {} + }, + "edition": "2021", + "proc_macro_deps": { + "common": [ + { + "id": "ctor 0.2.8", + "target": "ctor" + } + ], + "selects": {} + }, + "version": "1.0.0" + }, + "license": null, + "license_ids": [], + "license_file": "../LICENSE" + }, "bd-shutdown 1.0.0": { "name": "bd-shutdown", "version": "1.0.0", @@ -3996,7 +4105,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-shutdown" } @@ -4048,7 +4157,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-stats-common" } @@ -4087,7 +4196,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-test-helpers" } @@ -4173,6 +4282,10 @@ "id": "bd-session 1.0.0", "target": "bd_session" }, + { + "id": "bd-session-replay 1.0.0", + "target": "bd_session_replay" + }, { "id": "bd-time 1.0.0", "target": "bd_time" @@ -4248,7 +4361,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-time" } @@ -4321,7 +4434,7 @@ "Git": { "remote": "https://github.com/bitdriftlabs/shared-core.git", "commitish": { - "Rev": "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" + "Rev": "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" }, "strip_prefix": "bd-workflows" } diff --git a/Cargo.lock b/Cargo.lock index 727ac7e4..069640e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,7 +231,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bd-api" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "async-trait", @@ -260,7 +260,7 @@ dependencies = [ [[package]] name = "bd-buffer" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "async-trait", @@ -287,7 +287,7 @@ dependencies = [ [[package]] name = "bd-client-common" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "bd-client-stats-store", @@ -309,7 +309,7 @@ dependencies = [ [[package]] name = "bd-client-stats" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "async-trait", @@ -333,7 +333,7 @@ dependencies = [ [[package]] name = "bd-client-stats-store" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "bd-proto", "bd-stats-common", @@ -347,7 +347,7 @@ dependencies = [ [[package]] name = "bd-completion" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "log", @@ -357,7 +357,7 @@ dependencies = [ [[package]] name = "bd-device" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "bd-client-common", @@ -374,7 +374,7 @@ dependencies = [ [[package]] name = "bd-events" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "bd-runtime", "bd-shutdown", @@ -386,7 +386,7 @@ dependencies = [ [[package]] name = "bd-grpc" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "async-trait", @@ -423,7 +423,7 @@ dependencies = [ [[package]] name = "bd-grpc-codec" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "bd-client-common", @@ -438,7 +438,7 @@ dependencies = [ [[package]] name = "bd-hyper-network" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "async-trait", @@ -460,7 +460,7 @@ dependencies = [ [[package]] name = "bd-internal-logging" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "bd-log-primitives", @@ -472,7 +472,7 @@ dependencies = [ [[package]] name = "bd-key-value" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "base64", @@ -487,7 +487,7 @@ dependencies = [ [[package]] name = "bd-log" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "bd-time", @@ -504,7 +504,7 @@ dependencies = [ [[package]] name = "bd-log-filter" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "bd-client-stats-store", @@ -520,7 +520,7 @@ dependencies = [ [[package]] name = "bd-log-matcher" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "bd-log-primitives", @@ -534,7 +534,7 @@ dependencies = [ [[package]] name = "bd-log-metadata" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "bd-log-primitives", @@ -544,7 +544,7 @@ dependencies = [ [[package]] name = "bd-log-primitives" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "bd-proto", @@ -554,7 +554,7 @@ dependencies = [ [[package]] name = "bd-logger" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "async-trait", @@ -580,6 +580,7 @@ dependencies = [ "bd-resource-utilization", "bd-runtime", "bd-session", + "bd-session-replay", "bd-shutdown", "bd-stats-common", "bd-time", @@ -602,7 +603,7 @@ dependencies = [ [[package]] name = "bd-matcher" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "bd-log-primitives", @@ -615,7 +616,7 @@ dependencies = [ [[package]] name = "bd-metadata" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "base64", @@ -630,12 +631,12 @@ dependencies = [ [[package]] name = "bd-network-quality" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" [[package]] name = "bd-noop-network" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "async-trait", @@ -646,7 +647,7 @@ dependencies = [ [[package]] name = "bd-pgv" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "log", "protobuf", @@ -657,7 +658,7 @@ dependencies = [ [[package]] name = "bd-proto" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "bd-pgv", "bytes", @@ -670,7 +671,7 @@ dependencies = [ [[package]] name = "bd-resource-utilization" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "bd-internal-logging", @@ -685,7 +686,7 @@ dependencies = [ [[package]] name = "bd-runtime" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "bd-client-common", @@ -699,7 +700,7 @@ dependencies = [ [[package]] name = "bd-server-stats" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "bd-stats-common", "bd-time", @@ -718,7 +719,7 @@ dependencies = [ [[package]] name = "bd-session" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "bd-client-common", @@ -735,10 +736,31 @@ dependencies = [ "uuid", ] +[[package]] +name = "bd-session-replay" +version = "1.0.0" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" +dependencies = [ + "anyhow", + "bd-client-common", + "bd-client-stats-store", + "bd-internal-logging", + "bd-log-primitives", + "bd-runtime", + "bd-shutdown", + "bd-stats-common", + "bd-time", + "ctor", + "log", + "parking_lot", + "time", + "tokio", +] + [[package]] name = "bd-shutdown" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "log", "tokio", @@ -747,12 +769,12 @@ dependencies = [ [[package]] name = "bd-stats-common" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" [[package]] name = "bd-test-helpers" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "async-trait", @@ -770,6 +792,7 @@ dependencies = [ "bd-proto", "bd-resource-utilization", "bd-session", + "bd-session-replay", "bd-time", "futures-core", "http-body-util", @@ -787,7 +810,7 @@ dependencies = [ [[package]] name = "bd-time" version = "1.0.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "async-trait", "parking_lot", @@ -800,7 +823,7 @@ dependencies = [ [[package]] name = "bd-workflows" version = "0.1.0" -source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1020e6b4928026bdadf8b8e4e2c1e5c71c350059#1020e6b4928026bdadf8b8e4e2c1e5c71c350059" +source = "git+https://github.com/bitdriftlabs/shared-core.git?rev=1db88011bfc5c1f7c1888b6a95db8c3d82bc9013#1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 3d1e3462..fa3a6814 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,25 +17,25 @@ android_logger = { version = "0.14.1", default-features = false } anyhow = "1.0.90" assert_matches = "1.5.0" async-trait = "0.1.83" -bd-api = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-buffer = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-client-common = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-client-stats-store = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-device = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-grpc = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-hyper-network = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-key-value = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-log = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-log-metadata = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-log-primitives = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-logger = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-noop-network = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-proto = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-runtime = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-session = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-shutdown = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } -bd-test-helpers = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059", default-features = false } -bd-time = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1020e6b4928026bdadf8b8e4e2c1e5c71c350059" } +bd-api = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-buffer = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-client-common = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-client-stats-store = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-device = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-grpc = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-hyper-network = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-key-value = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-log = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-log-metadata = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-log-primitives = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-logger = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-noop-network = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-proto = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-runtime = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-session = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-shutdown = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } +bd-test-helpers = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013", default-features = false } +bd-time = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "1db88011bfc5c1f7c1888b6a95db8c3d82bc9013" } chrono = "0.4.38" clap = { version = "4.5.20", features = ["derive", "env"] } ctor = "0.2.8" diff --git a/examples/android/MainActivity.kt b/examples/android/MainActivity.kt index 1d25d77a..6d48e6e3 100644 --- a/examples/android/MainActivity.kt +++ b/examples/android/MainActivity.kt @@ -31,8 +31,6 @@ import com.github.michaelbull.result.onSuccess import io.bitdrift.capture.Capture.Logger import io.bitdrift.capture.LogLevel import io.bitdrift.capture.common.ErrorHandler -import io.bitdrift.capture.common.Runtime -import io.bitdrift.capture.common.RuntimeFeature import io.bitdrift.capture.network.okhttp.CaptureOkHttpEventListenerFactory import io.bitdrift.capture.replay.ReplayLogger import io.bitdrift.capture.replay.ReplayModule @@ -48,7 +46,6 @@ 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 @@ -85,12 +82,8 @@ class MainActivity : ComponentActivity() { } }, SessionReplayConfiguration(), - object: Runtime { - override fun isEnabled(feature: RuntimeFeature): Boolean { - return true - } - } - ), this.applicationContext) + this.applicationContext + )) } private lateinit var clipboardManager: ClipboardManager private lateinit var client: OkHttpClient diff --git a/examples/swift/hello_world/LoggerCustomer.swift b/examples/swift/hello_world/LoggerCustomer.swift index 1f889770..45905e66 100644 --- a/examples/swift/hello_world/LoggerCustomer.swift +++ b/examples/swift/hello_world/LoggerCustomer.swift @@ -91,7 +91,6 @@ final class LoggerCustomer: NSObject, URLSessionDelegate { .start( withAPIKey: Configuration.storedAPIKey ?? "", sessionStrategy: .fixed(), - configuration: .init(), fieldProviders: [CustomFieldProvider()], apiURL: apiURL )? diff --git a/platform/jvm/capture/consumer-rules.pro b/platform/jvm/capture/consumer-rules.pro index 454ca543..0089fa01 100644 --- a/platform/jvm/capture/consumer-rules.pro +++ b/platform/jvm/capture/consumer-rules.pro @@ -60,6 +60,10 @@ public ; } +-keep, includedescriptorclasses class io.bitdrift.capture.ISessionReplayTarget { + public ; +} + -keepclasseswithmembernames,includedescriptorclasses class * { native ; } 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 d5331de4..8187caff 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 @@ -42,6 +42,7 @@ internal object CaptureJniLibrary : IBridge { * @param sessionStrategy the session strategy to use. * @param metadataProvider used to provide metadata for emitted logs. * @param resourceUtilizationTarget used to inform platform layer about a need to emit a resource log. + * @param sessionReplayTarget used to inform platform layer about a need to emit session replay logs. * @param eventsListenerTarget responsible for listening to platform events and emitting logs in response to them. * @param applicationId the application ID of the current app, used to identify with the backend * @param applicationVersion the version of the current app, used to identify with the backend @@ -55,6 +56,7 @@ internal object CaptureJniLibrary : IBridge { sessionStrategy: SessionStrategyConfiguration, metadataProvider: IMetadataProvider, resourceUtilizationTarget: IResourceUtilizationTarget, + sessionReplayTarget: ISessionReplayTarget, eventsListenerTarget: IEventsListenerTarget, applicationId: String, applicationVersion: String, @@ -167,7 +169,7 @@ internal object CaptureJniLibrary : IBridge { * @param fields the fields to include with the log. * @param durationMs the duration of time the preparation of the session replay log took. */ - external fun writeSessionReplayLog( + external fun writeSessionReplayScreenLog( loggerId: Long, fields: Map, duration: Double, diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Configuration.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Configuration.kt index 78dcf274..a102fefb 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Configuration.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Configuration.kt @@ -14,5 +14,5 @@ import io.bitdrift.capture.replay.SessionReplayConfiguration * @param sessionReplayConfiguration The resource reporting configuration to use. Passing `null` disables the feature. */ data class Configuration @JvmOverloads constructor( - val sessionReplayConfiguration: SessionReplayConfiguration? = SessionReplayConfiguration(), + val sessionReplayConfiguration: SessionReplayConfiguration = SessionReplayConfiguration(), ) diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/IBridge.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/IBridge.kt index 89a04c63..c8d6074d 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/IBridge.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/IBridge.kt @@ -18,6 +18,7 @@ internal interface IBridge { sessionStrategy: SessionStrategyConfiguration, metadataProvider: IMetadataProvider, resourceUtilizationTarget: IResourceUtilizationTarget, + sessionReplayTarget: ISessionReplayTarget, eventsListenerTarget: IEventsListenerTarget, applicationId: String, applicationVersion: String, diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/ISessionReplayTarget.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/ISessionReplayTarget.kt new file mode 100644 index 00000000..f8208479 --- /dev/null +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/ISessionReplayTarget.kt @@ -0,0 +1,26 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +package io.bitdrift.capture + +/** + * Responsible for emitting session replay screen and screenshot logs. + */ +interface ISessionReplayTarget { + /** + * Called to indicate that the target is supposed to prepare and emit a session replay screen log. + */ + fun captureScreen() + + /** + * Called to indicate that the target should prepare and emit a session replay screenshot log. + * The Rust logger does not request another screenshot until it receives the previously + * requested one. This mechanism is designed to ensure that there are no situations where + * the Rust logger requests screenshots at a rate faster than the platform layer can handle. + */ + fun captureScreenshot() +} 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 f9240024..815cdc0b 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 @@ -22,7 +22,7 @@ import io.bitdrift.capture.common.RuntimeFeature import io.bitdrift.capture.error.ErrorReporterService import io.bitdrift.capture.error.IErrorReporter import io.bitdrift.capture.events.AppUpdateListenerLogger -import io.bitdrift.capture.events.ReplayScreenLogger +import io.bitdrift.capture.events.SessionReplayTarget import io.bitdrift.capture.events.common.PowerMonitor import io.bitdrift.capture.events.device.DeviceStateListenerLogger import io.bitdrift.capture.events.lifecycle.AppExitLogger @@ -96,7 +96,7 @@ internal class LoggerImpl( private val resourceUtilizationTarget: ResourceUtilizationTarget private val eventsListenerTarget = EventsListenerTarget() - private val replayScreenLogger: ReplayScreenLogger? + private val sessionReplayTarget: SessionReplayTarget? @VisibleForTesting internal val loggerId: LoggerId @@ -147,16 +147,26 @@ internal class LoggerImpl( processingQueue, ) + val sessionReplayTarget = SessionReplayTarget( + configuration = configuration.sessionReplayConfiguration, + errorHandler, + context, + logger = this, + ) + + this.sessionReplayTarget = sessionReplayTarget + val loggerId = bridge.createLogger( sdkDirectory, apiKey, sessionStrategy.createSessionStrategyConfiguration { appExitSaveCurrentSessionId(it) }, metadataProvider, - // TODO(Augustyniak): Pass `resourceUtilizationTarget` and `eventsListenerTarget` - // as part of `startLogger` method call instead. + // TODO(Augustyniak): Pass `resourceUtilizationTarget`, `sessionReplayTarget`, + // and `eventsListenerTarget` as part of `startLogger` method call instead. // Pass the event listener target here and finish setting up // before the logger is actually started. resourceUtilizationTarget, + sessionReplayTarget, // Pass the event listener target here and finish setting up // before the logger is actually started. eventsListenerTarget, @@ -172,6 +182,7 @@ internal class LoggerImpl( this.loggerId = loggerId runtime = JniRuntime(this.loggerId) + sessionReplayTarget.runtime = runtime diskUsageMonitor.runtime = runtime eventsListenerTarget.add( @@ -227,18 +238,6 @@ internal class LoggerImpl( appExitLogger.installAppExitLogger() CaptureJniLibrary.startLogger(this.loggerId) - - this.replayScreenLogger = configuration.sessionReplayConfiguration?.let { - ReplayScreenLogger( - errorHandler, - context, - logger = this, - processLifecycleOwner = ProcessLifecycleOwner.get(), - configuration = it, - runtime = runtime, - ) - } - this.replayScreenLogger?.start() } CaptureJniLibrary.writeSDKStartLog( @@ -373,8 +372,8 @@ internal class LoggerImpl( } } - internal fun logSessionReplay(fields: Map, duration: Duration) { - CaptureJniLibrary.writeSessionReplayLog( + internal fun logSessionReplayScreen(fields: Map, duration: Duration) { + CaptureJniLibrary.writeSessionReplayScreenLog( this.loggerId, fields, duration.toDouble(DurationUnit.SECONDS), @@ -422,7 +421,6 @@ internal class LoggerImpl( @Suppress("UnusedPrivateMember") private fun stopLoggingDefaultEvents() { appExitLogger.uninstallAppExitLogger() - replayScreenLogger?.stop() } /** diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/ReplayScreenLogger.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/SessionReplayTarget.kt similarity index 60% rename from platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/ReplayScreenLogger.kt rename to platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/SessionReplayTarget.kt index 09858890..ab8f330d 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/ReplayScreenLogger.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/SessionReplayTarget.kt @@ -8,15 +8,14 @@ package io.bitdrift.capture.events import android.content.Context -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.LifecycleOwner +import io.bitdrift.capture.ISessionReplayTarget import io.bitdrift.capture.LogLevel import io.bitdrift.capture.LogType import io.bitdrift.capture.LoggerImpl import io.bitdrift.capture.common.ErrorHandler import io.bitdrift.capture.common.MainThreadHandler import io.bitdrift.capture.common.Runtime +import io.bitdrift.capture.common.RuntimeFeature import io.bitdrift.capture.providers.FieldValue import io.bitdrift.capture.providers.toFieldValue import io.bitdrift.capture.providers.toFields @@ -27,37 +26,35 @@ import io.bitdrift.capture.replay.internal.EncodedScreenMetrics import io.bitdrift.capture.replay.internal.FilteredCapture // Controls the replay feature -internal class ReplayScreenLogger( +internal class SessionReplayTarget( + configuration: SessionReplayConfiguration, errorHandler: ErrorHandler, - private val context: Context, + context: Context, private val logger: LoggerImpl, - private val processLifecycleOwner: LifecycleOwner, - private val mainThreadHandler: MainThreadHandler = MainThreadHandler(), - runtime: Runtime, - configuration: SessionReplayConfiguration, -) : LifecycleEventObserver, ReplayLogger { - - private val replayModule: ReplayModule = ReplayModule(errorHandler, this, configuration, runtime) - - fun start() { - mainThreadHandler.run { - processLifecycleOwner.lifecycle.addObserver(this) - } - } + mainThreadHandler: MainThreadHandler = MainThreadHandler(), +) : ISessionReplayTarget, ReplayLogger { + // TODO(Augustyniak): Make non nullable and pass at initialization time after + // `sessionReplayTarget` argument is moved from logger creation time to logger start time. + // Refer to TODO in `LoggerImpl` for more details. + internal var runtime: Runtime? = null + private val replayModule: ReplayModule = ReplayModule( + errorHandler, + this, + configuration, + context, + mainThreadHandler, + ) - fun stop() { - mainThreadHandler.run { - processLifecycleOwner.lifecycle.removeObserver(this) - } + override fun captureScreen() { + val skipReplayComposeViews = runtime?.isEnabled(RuntimeFeature.SESSION_REPLAY_COMPOSE) + ?: RuntimeFeature.SESSION_REPLAY_COMPOSE.defaultValue + replayModule.captureScreen(skipReplayComposeViews) } - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - when (event) { - Lifecycle.Event.ON_CREATE -> replayModule.create(context) - Lifecycle.Event.ON_START -> replayModule.start() - Lifecycle.Event.ON_STOP -> replayModule.stop() - else -> {} - } + override fun captureScreenshot() { + // TODO(Augustyniak): Implement this method to add support for screenshot capture on Android. + // As currently implemented, the function must emit a session replay screenshot log. + // Without this emission, the SDK is blocked from requesting additional screenshots. } override fun onScreenCaptured(encodedScreen: ByteArray, screen: FilteredCapture, metrics: EncodedScreenMetrics) { @@ -66,7 +63,7 @@ internal class ReplayScreenLogger( putAll(metrics.toMap().toFields()) } - logger.logSessionReplay(fields, metrics.parseDuration) + logger.logSessionReplayScreen(fields, metrics.parseDuration) } override fun logVerboseInternal(message: String, fields: Map?) { diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerNetworkTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerNetworkTest.kt index f614dd1a..b852e263 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerNetworkTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerNetworkTest.kt @@ -77,6 +77,7 @@ class CaptureLoggerNetworkTest { loggerBridge, mock(), mock(), + mock(), "test", "test", network, @@ -155,6 +156,7 @@ class CaptureLoggerNetworkTest { loggerBridge, mock(), mock(), + mock(), "test", "test", network, @@ -183,6 +185,7 @@ class CaptureLoggerNetworkTest { loggerBridge, mock(), mock(), + mock(), "test", "test", network, diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerSessionOverrideTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerSessionOverrideTest.kt index b4e26e96..cd4f6fba 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerSessionOverrideTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerSessionOverrideTest.kt @@ -77,9 +77,7 @@ class CaptureLoggerSessionOverrideTest { fieldProviders = listOf(), dateProvider = systemDateProvider, sessionStrategy = SessionStrategy.Fixed { "foo" }, - configuration = Configuration( - sessionReplayConfiguration = null, - ), + configuration = Configuration(), preferences = preferences, ) @@ -109,9 +107,7 @@ class CaptureLoggerSessionOverrideTest { fieldProviders = listOf(), dateProvider = systemDateProvider, sessionStrategy = SessionStrategy.Fixed { "bar" }, - configuration = Configuration( - sessionReplayConfiguration = null, - ), + configuration = Configuration(), preferences = preferences, activityManager = activityManager, ) diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerTest.kt index e8af0bb4..5fa76162 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerTest.kt @@ -74,7 +74,7 @@ class CaptureLoggerTest { fieldProviders = listOf(), dateProvider = systemDateProvider, sessionStrategy = SessionStrategy.Fixed { "SESSION_ID" }, - configuration = Configuration(sessionReplayConfiguration = null), + configuration = Configuration(), preferences = MockPreferences(), ) } @@ -344,7 +344,7 @@ class CaptureLoggerTest { fieldProviders = listOf(fieldProvider), sessionStrategy = SessionStrategy.Fixed { "SESSION_ID" }, dateProvider = dateProvider, - configuration = Configuration(null), + configuration = Configuration(), ), ) @@ -452,7 +452,7 @@ class CaptureLoggerTest { @Test fun jni_runtime() { - assertThat(JniRuntime(logger.loggerId).isEnabled(RuntimeFeature.SESSION_REPLAY)).isTrue + assertThat(JniRuntime(logger.loggerId).isEnabled(RuntimeFeature.SESSION_REPLAY_COMPOSE)).isTrue val streamId = CaptureTestJniLibrary.awaitNextApiStream() assertThat(streamId).isNotEqualTo(-1) @@ -461,10 +461,10 @@ class CaptureLoggerTest { CaptureTestJniLibrary.disableRuntimeFeature( streamId, - RuntimeFeature.SESSION_REPLAY.featureName, + RuntimeFeature.SESSION_REPLAY_COMPOSE.featureName, ) - assertThat(JniRuntime(logger.loggerId).isEnabled(RuntimeFeature.SESSION_REPLAY)).isFalse + assertThat(JniRuntime(logger.loggerId).isEnabled(RuntimeFeature.SESSION_REPLAY_COMPOSE)).isFalse } private fun testServerUrl(): HttpUrl { diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureTestJniLibrary.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureTestJniLibrary.kt index e9ddd5fe..87fd28f8 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureTestJniLibrary.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureTestJniLibrary.kt @@ -73,10 +73,13 @@ object CaptureTestJniLibrary { // Runs key value storage tests. external fun runKeyValueStorageTest(preferences: Any) - // Runs resource utilization target tests. + // Runs resource utilization target test. external fun runResourceUtilizationTargetTest(target: Any) - // Runs events listener target tests. + // Runs session replay target test. + external fun runSessionReplayTargetTest(target: Any) + + // Runs events listener target test. external fun runEventsListenerTargetTest(target: Any) // Issues a runtime update causing the specified feature to be marked as disabled. diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ConfigurationTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ConfigurationTest.kt index ad0cb36d..2a99d878 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ConfigurationTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ConfigurationTest.kt @@ -43,6 +43,7 @@ class ConfigurationTest { anyOrNull(), anyOrNull(), anyOrNull(), + anyOrNull(), ), ).thenReturn(-1L) @@ -72,6 +73,7 @@ class ConfigurationTest { anyOrNull(), anyOrNull(), anyOrNull(), + anyOrNull(), ) // We perform another attempt to configure the logger to verify that @@ -98,6 +100,7 @@ class ConfigurationTest { anyOrNull(), anyOrNull(), anyOrNull(), + anyOrNull(), ) } diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionReplayTargetTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionReplayTargetTest.kt new file mode 100644 index 00000000..328dbd0b --- /dev/null +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionReplayTargetTest.kt @@ -0,0 +1,52 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +package io.bitdrift.capture + +import androidx.test.core.app.ApplicationProvider +import com.nhaarman.mockitokotlin2.mock +import io.bitdrift.capture.common.ErrorHandler +import io.bitdrift.capture.common.MainThreadHandler +import io.bitdrift.capture.events.SessionReplayTarget +import io.bitdrift.capture.replay.SessionReplayConfiguration +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21]) +class SessionReplayTargetTest { + private val logger: LoggerImpl = mock() + private val errorHandler: ErrorHandler = mock() + private val handler: MainThreadHandler = Mocks.sameThreadHandler + + private val target = SessionReplayTarget( + errorHandler = errorHandler, + context = ApplicationProvider.getApplicationContext(), + logger = logger, + configuration = SessionReplayConfiguration(), + mainThreadHandler = handler, + ) + + init { + CaptureJniLibrary.load() + } + + @Test + fun sessionReplayTargetDoesNotCrash() { + CaptureTestJniLibrary.runSessionReplayTargetTest(target) + } + + @Test + fun sessionReplayTargetEmitsScreenLog() { + target.captureScreen() + // TODO: Make this test work, the issue is that in test environment session replay + // sees 0 views and as a result it doesn't emit a session replay screen log. +// verify(logger, timeout(1000).times(1)).logSessionReplayScreen(any(), any()) + } +} diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionStrategyTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionStrategyTest.kt index 42b1c297..4beb6d3e 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionStrategyTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionStrategyTest.kt @@ -44,9 +44,7 @@ class SessionStrategyTest { generatedSessionIds.add(sessionId) sessionId }, - configuration = Configuration( - sessionReplayConfiguration = null, - ), + configuration = Configuration(), ) val sessionId = logger.sessionId @@ -76,9 +74,7 @@ class SessionStrategyTest { observedSessionId = it strategyLatch.countDown() }, - configuration = Configuration( - sessionReplayConfiguration = null, - ), + configuration = Configuration(), preferences = mock(), ) diff --git a/platform/jvm/common/src/main/kotlin/io/bitdrift/capture/common/Runtime.kt b/platform/jvm/common/src/main/kotlin/io/bitdrift/capture/common/Runtime.kt index dbce91ff..1fde0e8e 100644 --- a/platform/jvm/common/src/main/kotlin/io/bitdrift/capture/common/Runtime.kt +++ b/platform/jvm/common/src/main/kotlin/io/bitdrift/capture/common/Runtime.kt @@ -15,11 +15,6 @@ package io.bitdrift.capture.common * @param defaultValue whether the feature is enabled by default */ sealed class RuntimeFeature(val featureName: String, val defaultValue: Boolean = true) { - /** - * Whether the session replay feature is enabled. - */ - data object SESSION_REPLAY : RuntimeFeature("client_feature.android.session_replay") - /** * Whether the session replay feature is enabled for Compose views. */ diff --git a/platform/jvm/gradle-test-app/src/androidTest/java/io/bitdrift/gradletestapp/TestUtils.kt b/platform/jvm/gradle-test-app/src/androidTest/java/io/bitdrift/gradletestapp/TestUtils.kt index 78730060..a86d54c5 100644 --- a/platform/jvm/gradle-test-app/src/androidTest/java/io/bitdrift/gradletestapp/TestUtils.kt +++ b/platform/jvm/gradle-test-app/src/androidTest/java/io/bitdrift/gradletestapp/TestUtils.kt @@ -11,6 +11,7 @@ import android.content.Context import android.util.Base64 import android.util.Log import io.bitdrift.capture.common.ErrorHandler +import io.bitdrift.capture.common.MainThreadHandler import io.bitdrift.capture.common.Runtime import io.bitdrift.capture.common.RuntimeFeature import io.bitdrift.capture.replay.ReplayLogger @@ -71,14 +72,10 @@ object TestUtils { Log.e("Replay Tests", message, e) } }, - // The capture frequency is ignored when using ReplayPreviewClient - SessionReplayConfiguration(captureIntervalMs = 10000), - object : Runtime { - override fun isEnabled(feature: RuntimeFeature): Boolean { - return true - } - }), - context + SessionReplayConfiguration(), + context, + MainThreadHandler(), + ), ) } } diff --git a/platform/jvm/jni_symbols.lds b/platform/jvm/jni_symbols.lds index 212d304f..b8d7a169 100644 --- a/platform/jvm/jni_symbols.lds +++ b/platform/jvm/jni_symbols.lds @@ -10,7 +10,7 @@ Java_io_bitdrift_capture_CaptureJniLibrary_getDeviceId Java_io_bitdrift_capture_CaptureJniLibrary_addLogField Java_io_bitdrift_capture_CaptureJniLibrary_removeLogField Java_io_bitdrift_capture_CaptureJniLibrary_writeLog -Java_io_bitdrift_capture_CaptureJniLibrary_writeSessionReplayLog +Java_io_bitdrift_capture_CaptureJniLibrary_writeSessionReplayScreenLog Java_io_bitdrift_capture_CaptureJniLibrary_writeResourceUtilizationLog Java_io_bitdrift_capture_CaptureJniLibrary_writeSDKStartLog Java_io_bitdrift_capture_CaptureJniLibrary_shouldWriteAppUpdateLog diff --git a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayMapperConfiguration.kt b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayMapperConfiguration.kt index 82a9d6f7..134417e2 100644 --- a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayMapperConfiguration.kt +++ b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayMapperConfiguration.kt @@ -12,5 +12,5 @@ package io.bitdrift.capture.replay * @param customMapper list of known types */ class ReplayMapperConfiguration( - val customMapper: Map>? = null, + val customMapper: Map>, ) diff --git a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayModule.kt b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayModule.kt index 909c514b..604435f6 100644 --- a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayModule.kt +++ b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayModule.kt @@ -9,7 +9,7 @@ package io.bitdrift.capture.replay import android.content.Context import io.bitdrift.capture.common.ErrorHandler -import io.bitdrift.capture.common.Runtime +import io.bitdrift.capture.common.MainThreadHandler import io.bitdrift.capture.replay.internal.ReplayCaptureController import io.bitdrift.capture.replay.internal.ReplayDependencies @@ -19,15 +19,15 @@ internal typealias L = ReplayModuleInternalLogs internal object ReplayModuleInternalLogs { fun v(message: String) { - ReplayModule.replayDependencies.replayLogger.logVerboseInternal(message) + ReplayModule.replayDependencies.logger.logVerboseInternal(message) } fun d(message: String) { - ReplayModule.replayDependencies.replayLogger.logDebugInternal(message) + ReplayModule.replayDependencies.logger.logDebugInternal(message) } fun e(e: Throwable?, message: String) { - ReplayModule.replayDependencies.replayLogger.logErrorInternal(message, e) + ReplayModule.replayDependencies.logger.logErrorInternal(message, e) } } @@ -40,43 +40,32 @@ internal object ReplayModuleInternalLogs { */ class ReplayModule( errorHandler: ErrorHandler, - internal val replayLogger: ReplayLogger, + internal val logger: ReplayLogger, sessionReplayConfiguration: SessionReplayConfiguration, - val runtime: Runtime, + context: Context, + mainThreadHandler: MainThreadHandler = MainThreadHandler(), ) { - private lateinit var replayCaptureController: ReplayCaptureController + private var replayCaptureController: ReplayCaptureController init { replayDependencies = ReplayDependencies( errorHandler = errorHandler, - replayLogger = replayLogger, + logger = logger, sessionReplayConfiguration = sessionReplayConfiguration, ) - } - - /** - * Creates the replay feature - */ - fun create(context: Context) { + replayDependencies.displayManager.init(context) replayCaptureController = ReplayCaptureController( - sessionReplayConfiguration = replayDependencies.sessionReplayConfiguration, - runtime = runtime, + logger = logger, + mainThreadHandler = mainThreadHandler, ) - replayDependencies.displayManager.init(context) - } - - /** - * Starts capturing screens periodically using the given configuration - */ - fun start() { - replayCaptureController.start() } /** - * Stops capturing screens + * Prepares and emits a session replay screen log using a logger instance passed + * at initialiation time. */ - fun stop() { - replayCaptureController.stop() + fun captureScreen(skipReplayComposeViews: Boolean) { + replayCaptureController.captureScreen(skipReplayComposeViews) } companion object { diff --git a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayPreviewClient.kt b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayPreviewClient.kt index 6fa09316..bb5d751b 100644 --- a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayPreviewClient.kt +++ b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayPreviewClient.kt @@ -7,7 +7,6 @@ package io.bitdrift.capture.replay -import android.content.Context import android.util.Base64 import android.util.Log import io.bitdrift.capture.replay.internal.EncodedScreenMetrics @@ -34,13 +33,12 @@ import java.util.concurrent.TimeUnit */ class ReplayPreviewClient( private val replayModule: ReplayModule, - context: Context, protocol: String = "ws", host: String = "10.0.2.2", port: Int = 3001, ) : ReplayLogger { - private val replayCapture: ReplayCapture = ReplayCapture(this) + private val replayCapture: ReplayCapture = ReplayCapture() private val executor: ExecutorService = Executors.newSingleThreadExecutor { Thread(it, "io.bitdrift.capture.session-replay-client") } @@ -51,7 +49,6 @@ class ReplayPreviewClient( init { // Calling this is necessary to capture the display size - replayModule.create(context) client = OkHttpClient.Builder() .readTimeout(0, TimeUnit.MILLISECONDS) .build() @@ -76,7 +73,9 @@ class ReplayPreviewClient( * Capture the screen and send it over the websocket connection after processing */ fun captureScreen() { - replayCapture.captureScreen(executor, skipReplayComposeViews = false) + replayCapture.captureScreen(executor, skipReplayComposeViews = false) { encodedScreen, screen, metrics -> + replayModule.logger.onScreenCaptured(encodedScreen, screen, metrics) + } } /** @@ -95,19 +94,19 @@ class ReplayPreviewClient( lastEncodedScreen = encodedScreen webSocket?.send(encodedScreen.toByteString(0, encodedScreen.size)) // forward the callback to the module's logger - replayModule.replayLogger.onScreenCaptured(encodedScreen, screen, metrics) + replayModule.logger.onScreenCaptured(encodedScreen, screen, metrics) } override fun logVerboseInternal(message: String, fields: Map?) { - replayModule.replayLogger.logVerboseInternal(message, fields) + replayModule.logger.logVerboseInternal(message, fields) } override fun logDebugInternal(message: String, fields: Map?) { - replayModule.replayLogger.logDebugInternal(message, fields) + replayModule.logger.logDebugInternal(message, fields) } override fun logErrorInternal(message: String, e: Throwable?, fields: Map?) { - replayModule.replayLogger.logErrorInternal(message, e, fields) + replayModule.logger.logErrorInternal(message, e, fields) } private object WebSocketLogger : WebSocketListener() { diff --git a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/SessionReplayConfiguration.kt b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/SessionReplayConfiguration.kt index 7df8298c..cc3d7901 100644 --- a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/SessionReplayConfiguration.kt +++ b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/SessionReplayConfiguration.kt @@ -9,10 +9,8 @@ package io.bitdrift.capture.replay /** * A configuration used to configure Bitdrift session replay feature. - * @param captureIntervalMs The number of milliseconds between consecutive screen replay captures. * @param replayMapperConfiguration Map used to matching third party Android views to Bitdrift view types. */ data class SessionReplayConfiguration @JvmOverloads constructor( - val captureIntervalMs: Long = 3000, val replayMapperConfiguration: ReplayMapperConfiguration? = null, ) diff --git a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayCapture.kt b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayCapture.kt index c013700c..2d4c2ca5 100644 --- a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayCapture.kt +++ b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayCapture.kt @@ -10,13 +10,11 @@ package io.bitdrift.capture.replay.internal import io.bitdrift.capture.common.DefaultClock import io.bitdrift.capture.common.IClock import io.bitdrift.capture.replay.L -import io.bitdrift.capture.replay.ReplayLogger import java.util.concurrent.ExecutorService import kotlin.time.measureTimedValue // This is the main logic for capturing a screen internal class ReplayCapture( - private val replayLogger: ReplayLogger, private val captureParser: ReplayParser = ReplayParser(), private val captureFilter: ReplayFilter = ReplayFilter(), private val captureDecorations: ReplayDecorations = ReplayDecorations(), @@ -24,7 +22,11 @@ internal class ReplayCapture( private val clock: IClock = DefaultClock.getInstance(), ) { - fun captureScreen(executor: ExecutorService, skipReplayComposeViews: Boolean) { + fun captureScreen( + executor: ExecutorService, + skipReplayComposeViews: Boolean, + completion: (encodedScreen: ByteArray, screen: FilteredCapture, metrics: EncodedScreenMetrics) -> Unit, + ) { val startTime = clock.elapsedRealtime() val encodedScreenMetrics = EncodedScreenMetrics() @@ -40,7 +42,7 @@ internal class ReplayCapture( val encodedScreen = replayEncoder.encode(screen) encodedScreenMetrics.captureTimeMs = clock.elapsedRealtime() - startTime L.d("Screen Captured: $encodedScreenMetrics") - replayLogger.onScreenCaptured(encodedScreen, screen, encodedScreenMetrics) + completion(encodedScreen, screen, encodedScreenMetrics) } } } diff --git a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayCaptureController.kt b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayCaptureController.kt index a6f41d64..b6b7f74c 100644 --- a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayCaptureController.kt +++ b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayCaptureController.kt @@ -8,46 +8,25 @@ package io.bitdrift.capture.replay.internal import io.bitdrift.capture.common.MainThreadHandler -import io.bitdrift.capture.common.Runtime -import io.bitdrift.capture.common.RuntimeFeature +import io.bitdrift.capture.replay.ReplayLogger import io.bitdrift.capture.replay.ReplayModule -import io.bitdrift.capture.replay.SessionReplayConfiguration import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit -// Controls the triggering of screen captures at regular time interval +// Captures wireframe and pixel perfect representations of app's screen. internal class ReplayCaptureController( - private val mainThreadHandler: MainThreadHandler = MainThreadHandler(), + private val mainThreadHandler: MainThreadHandler, private val executor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor { Thread(it, "io.bitdrift.capture.session-replay") }, - private val sessionReplayConfiguration: SessionReplayConfiguration = ReplayModule.replayDependencies.sessionReplayConfiguration, private val replayCapture: ReplayCapture = ReplayModule.replayDependencies.replayCapture, - private val runtime: Runtime, + private val logger: ReplayLogger, ) { - - private var executionHandle: ScheduledFuture<*>? = null - - fun start() { - executionHandle = executor.scheduleWithFixedDelay( - { captureScreen() }, - 0L, // initialDelay - sessionReplayConfiguration.captureIntervalMs, - TimeUnit.MILLISECONDS, - ) - } - - fun stop() { - executionHandle?.cancel(false) - } - - private fun captureScreen() { - if (!runtime.isEnabled(RuntimeFeature.SESSION_REPLAY)) { - return + fun captureScreen(skipReplayComposeViews: Boolean) { + mainThreadHandler.run { + replayCapture.captureScreen(executor, skipReplayComposeViews) { byteArray, screen, metrics -> + logger.onScreenCaptured(byteArray, screen, metrics) + } } - val skipReplayComposeViews = !runtime.isEnabled(RuntimeFeature.SESSION_REPLAY_COMPOSE) - mainThreadHandler.run { replayCapture.captureScreen(executor, skipReplayComposeViews) } } } diff --git a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayDependencies.kt b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayDependencies.kt index d9c11a4b..9bbc20d6 100644 --- a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayDependencies.kt +++ b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayDependencies.kt @@ -13,12 +13,12 @@ import io.bitdrift.capture.replay.SessionReplayConfiguration internal class ReplayDependencies( val errorHandler: ErrorHandler, - val replayLogger: ReplayLogger, + val logger: ReplayLogger, val sessionReplayConfiguration: SessionReplayConfiguration, ) { val displayManager: DisplayManagers = DisplayManagers() val replayCapture: ReplayCapture by lazy { - ReplayCapture(replayLogger) + ReplayCapture() } } diff --git a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayFilter.kt b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayFilter.kt index 859d6d75..b6e68717 100644 --- a/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayFilter.kt +++ b/platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayFilter.kt @@ -29,6 +29,7 @@ internal class ReplayFilter { } // This capture is identical to the previous one, or is empty, filter it out + // One interesting case when capture is empty is when the application is backgrounded. return if (filteredCapture == previousCapture || filteredCapture.isEmpty()) { null } else { diff --git a/platform/jvm/src/jni.rs b/platform/jvm/src/jni.rs index ef7f5c30..2e768c09 100644 --- a/platform/jvm/src/jni.rs +++ b/platform/jvm/src/jni.rs @@ -10,6 +10,7 @@ use crate::executor::{self}; use crate::key_value_storage::PreferencesHandle; use crate::resource_utilization::TargetHandler as ResourceUtilizationTargetHandler; use crate::session::SessionStrategyConfigurationHandle; +use crate::session_replay::{self, TargetHandler as SessionReplayTargetHandler}; use crate::{ define_object_wrapper, events, @@ -324,6 +325,7 @@ pub extern "system" fn JNI_OnLoad(vm: JavaVM, _: *mut c_void) -> jint { ffi::initialize(&mut env); session::initialize(&mut env); resource_utilization::initialize(&mut env); + session_replay::initialize(&mut env); env.get_version().unwrap().into() } @@ -593,6 +595,7 @@ pub extern "system" fn Java_io_bitdrift_capture_CaptureJniLibrary_createLogger( session_strategy: JObject<'_>, metadata_provider: JObject<'_>, resource_utilization_target: JObject<'_>, + session_replay_target: JObject<'_>, events_listener_target: JObject<'_>, application_id: JString<'_>, application_version: JString<'_>, @@ -647,6 +650,12 @@ pub extern "system" fn Java_io_bitdrift_capture_CaptureJniLibrary_createLogger( resource_utilization_target )?); + let session_replay_target = Box::new(new_global!( + SessionReplayTargetHandler, + &mut env, + session_replay_target + )?); + let events_listener_target = Box::new(new_global!( EventsListenerTargetHandler, &mut env, @@ -664,6 +673,7 @@ pub extern "system" fn Java_io_bitdrift_capture_CaptureJniLibrary_createLogger( session_strategy, metadata_provider: Arc::new(new_global!(MetadataProvider, &mut env, metadata_provider)?), resource_utilization_target, + session_replay_target, events_listener_target, device, store, @@ -857,7 +867,7 @@ pub extern "system" fn Java_io_bitdrift_capture_CaptureJniLibrary_writeLog( } #[no_mangle] -pub extern "system" fn Java_io_bitdrift_capture_CaptureJniLibrary_writeSessionReplayLog( +pub extern "system" fn Java_io_bitdrift_capture_CaptureJniLibrary_writeSessionReplayScreenLog( mut env: JNIEnv<'_>, _class: JClass<'_>, logger_id: jlong, @@ -875,7 +885,7 @@ pub extern "system" fn Java_io_bitdrift_capture_CaptureJniLibrary_writeSessionRe .collect(); let logger = unsafe { LoggerId::from_raw(logger_id) }; - logger.log_session_replay(fields, Duration::seconds_f64(duration_s)); + logger.log_session_replay_screen(fields, Duration::seconds_f64(duration_s)); Ok(()) }, diff --git a/platform/jvm/src/lib.rs b/platform/jvm/src/lib.rs index 84e024ec..9be8825b 100644 --- a/platform/jvm/src/lib.rs +++ b/platform/jvm/src/lib.rs @@ -12,3 +12,4 @@ pub mod jni; pub mod key_value_storage; pub mod resource_utilization; mod session; +pub mod session_replay; diff --git a/platform/jvm/src/session_replay.rs b/platform/jvm/src/session_replay.rs new file mode 100644 index 00000000..a181b827 --- /dev/null +++ b/platform/jvm/src/session_replay.rs @@ -0,0 +1,75 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +use crate::define_object_wrapper; +use crate::jni::{initialize_class, initialize_method_handle, CachedMethod}; +use jni::signature::{Primitive, ReturnType}; +use jni::JNIEnv; +use std::sync::OnceLock; + +static TARGET_CAPTURE_SCREEN: OnceLock = OnceLock::new(); +static TARGET_CAPTURE_SCREENSHOT: OnceLock = OnceLock::new(); + +pub(crate) fn initialize(env: &mut JNIEnv<'_>) { + let session_replay_target = + initialize_class(env, "io/bitdrift/capture/ISessionReplayTarget", None); + initialize_method_handle( + env, + &session_replay_target.class, + "captureScreen", + "()V", + &TARGET_CAPTURE_SCREEN, + ); + initialize_method_handle( + env, + &session_replay_target.class, + "captureScreenshot", + "()V", + &TARGET_CAPTURE_SCREENSHOT, + ); +} + +// +// TargetHandler +// + +define_object_wrapper!(TargetHandler); + +unsafe impl Send for TargetHandler {} +unsafe impl Sync for TargetHandler {} + +impl bd_logger::SessionReplayTarget for TargetHandler { + fn capture_screen(&self) { + bd_client_common::error::with_handle_unexpected( + || { + self.execute(|e, target| { + TARGET_CAPTURE_SCREEN + .get() + .unwrap() + .call_method(e, target, ReturnType::Primitive(Primitive::Void), &[]) + .map(|_| ()) + }) + }, + "session replay target_handler: capture screen", + ); + } + + fn capture_screenshot(&self) { + bd_client_common::error::with_handle_unexpected( + || { + self.execute(|e, target| { + TARGET_CAPTURE_SCREENSHOT + .get() + .unwrap() + .call_method(e, target, ReturnType::Primitive(Primitive::Void), &[]) + .map(|_| ()) + }) + }, + "session replay target_handler: capture screenshot", + ); + } +} diff --git a/platform/swift/source/CaptureRustBridge.h b/platform/swift/source/CaptureRustBridge.h index 12175a6c..e4324821 100644 --- a/platform/swift/source/CaptureRustBridge.h +++ b/platform/swift/source/CaptureRustBridge.h @@ -26,6 +26,7 @@ void capture_report_error(const char *message); * @param session_strategy_provider the session strategy provider. * @param metadata_provider used to provide the internal logger with logging metadata. * @param resource_utilization_target responsible for emitting resource utilization logs in response to provided ticks. + * @param session_replay_target responsible for emitting session replay logs in response to callbacks. * @param events_listener_target responsible for listening to platform events and emitting logs in response to them. * @param app_id the app id to identify the client as a null terminated C string. * @param app_version the app version to identify the client as a null terminated C string. @@ -38,6 +39,7 @@ logger_id capture_create_logger( id session_strategy_provider, id metadata_provider, id resource_utilization_target, + id session_replay_target, id events_listener_target, const char *app_id, const char *app_version, @@ -79,13 +81,26 @@ void capture_write_log( ); /* - * Writes a session replay log. + * Writes a session replay screen log. * * @param logger_id the ID of the logger to write to. * @param fields the fields to include with the log. * @param duration_s the duration of time the preparation of the session replay log took. */ -void capture_write_session_replay_log( +void capture_write_session_replay_screen_log( + logger_id logger_id, + const NSArray *fields, + double duration_s +); + +/* + * Writes a session replay screenshot log. + * + * @param logger_id the ID of the logger to write to. + * @param fields the fields to include with the log. + * @param duration_s the duration of time the preparation of the session replay log took. + */ +void capture_write_session_replay_screenshot_log( logger_id logger_id, const NSArray *fields, double duration_s diff --git a/platform/swift/source/CoreLogger.swift b/platform/swift/source/CoreLogger.swift index e26118aa..b79eba11 100644 --- a/platform/swift/source/CoreLogger.swift +++ b/platform/swift/source/CoreLogger.swift @@ -80,8 +80,15 @@ extension CoreLogger: CoreLogging { ) } - func logSessionReplay(screen: SessionReplayScreenCapture, duration: TimeInterval) { - self.underlyingLogger.logSessionReplay( + func logSessionReplayScreen(screen: SessionReplayCapture, duration: TimeInterval) { + self.underlyingLogger.logSessionReplayScreen( + fields: self.convertFields(fields: ["screen": screen]), + duration: duration + ) + } + + func logSessionReplayScreenshot(screen: SessionReplayCapture, duration: TimeInterval) { + self.underlyingLogger.logSessionReplayScreenshot( fields: self.convertFields(fields: ["screen": screen]), duration: duration ) diff --git a/platform/swift/source/CoreLogging.swift b/platform/swift/source/CoreLogging.swift index f96797f3..03695d2d 100644 --- a/platform/swift/source/CoreLogging.swift +++ b/platform/swift/source/CoreLogging.swift @@ -47,11 +47,17 @@ protocol CoreLogging: AnyObject { blocking: Bool ) - /// Writes a session replay log. + /// Writes a session replay screen log. /// - /// - parameter screen: The capture screen. + /// - parameter screen: The captured screen. /// - parameter duration: The duration of time the preparation of the log took. - func logSessionReplay(screen: SessionReplayScreenCapture, duration: TimeInterval) + func logSessionReplayScreen(screen: SessionReplayCapture, duration: TimeInterval) + + /// Writes a session replay screen log. + /// + /// - parameter screen: The captured screenshot. + /// - parameter duration: The duration of time the preparation of the log took. + func logSessionReplayScreenshot(screen: SessionReplayCapture, duration: TimeInterval) /// Writes a resource utilization log. /// diff --git a/platform/swift/source/Field+Extensions.swift b/platform/swift/source/Field+Extensions.swift index a2a4c0f7..d8c18c4f 100644 --- a/platform/swift/source/Field+Extensions.swift +++ b/platform/swift/source/Field+Extensions.swift @@ -32,7 +32,7 @@ extension Field { /// /// - returns: The created `Field` instance . static func make(key: String, value: FieldValue) throws -> Field { - if let value = value as? SessionReplayScreenCapture { + if let value = value as? SessionReplayCapture { return Field(key: key, data: value.data as NSData, type: .data) } else { let stringValue = try value.encodeToString() diff --git a/platform/swift/source/Logger.swift b/platform/swift/source/Logger.swift index a60d5b67..e2470e44 100644 --- a/platform/swift/source/Logger.swift +++ b/platform/swift/source/Logger.swift @@ -28,7 +28,7 @@ public final class Logger { private let remoteErrorReporter: RemoteErrorReporting private let deviceCodeController: DeviceCodeController - private(set) var replayController: ReplayController? + private(set) var sessionReplayTarget: SessionReplayTarget private(set) var dispatchSourceMemoryMonitor: DispatchSourceMemoryMonitor? private(set) var resourceUtilizationTarget: ResourceUtilizationTarget private(set) var eventsListenerTarget: EventsListenerTarget @@ -164,16 +164,20 @@ public final class Logger { ) self.eventsListenerTarget = EventsListenerTarget() + let sessionReplayTarget = SessionReplayTarget(configuration: configuration.sessionReplayConfiguration) + self.sessionReplayTarget = sessionReplayTarget + guard let logger = loggerBridgingFactoryProvider.makeLogger( apiKey: apiKey, bufferDirectoryPath: directoryURL?.path, sessionStrategy: sessionStrategy, metadataProvider: metadataProvider, - // TODO(Augustyniak): Pass `resourceUtilizationTarget` and `eventsListenerTarget` as part of - // the `self.underlyingLogger.start()` method call instead. + // TODO(Augustyniak): Pass `resourceUtilizationTarget`, `sessionReplayTarget`, + // and `eventsListenerTarget` as part of the `self.underlyingLogger.start()` method call instead. // Pass the event listener target here and finish setting up // before the logger is actually started. resourceUtilizationTarget: self.resourceUtilizationTarget, + sessionReplayTarget: self.sessionReplayTarget, // Pass the event listener target here and finish setting up // before the logger is actually started. eventsListenerTarget: self.eventsListenerTarget, @@ -204,6 +208,7 @@ public final class Logger { metadataProvider.errorHandler = { [weak underlyingLogger] context, error in underlyingLogger?.handleError(context: context, error: error) } + self.sessionReplayTarget.logger = self.underlyingLogger // Start attributes before the underlying logger is running to increase the chances // of out-of-the-box attributes being ready by the time logs emitted as a result of the logger start @@ -213,10 +218,6 @@ public final class Logger { self.underlyingLogger.start() - self.replayController = Self.setUpSessionReplay( - with: configuration.sessionReplayConfiguration, - logger: self.underlyingLogger - ) self.dispatchSourceMemoryMonitor = Self.setUpMemoryStateMonitoring(logger: self.underlyingLogger) self.deviceCodeController = DeviceCodeController(client: client) @@ -358,10 +359,8 @@ public final class Logger { } private func stop() { - self.replayController?.stop() self.dispatchSourceMemoryMonitor?.stop() - self.replayController = nil self.dispatchSourceMemoryMonitor = nil } } @@ -501,20 +500,6 @@ extension Logger: Logging { // MARK: - Features extension Logger { - private static func setUpSessionReplay( - with configuration: SessionReplayConfiguration?, - logger: CoreLogging - ) -> ReplayController? - { - guard let configuration else { - return nil - } - - let controller = ReplayController(logger: logger) - controller.start(with: configuration) - return controller - } - static func setUpMemoryStateMonitoring( logger: CoreLogging ) -> DispatchSourceMemoryMonitor diff --git a/platform/swift/source/LoggerBridge.swift b/platform/swift/source/LoggerBridge.swift index b3dfb3d6..0eb5cb60 100644 --- a/platform/swift/source/LoggerBridge.swift +++ b/platform/swift/source/LoggerBridge.swift @@ -22,6 +22,7 @@ final class LoggerBridge: LoggerBridging { sessionStrategy: SessionStrategy, metadataProvider: CaptureLoggerBridge.MetadataProvider, resourceUtilizationTarget: CaptureLoggerBridge.ResourceUtilizationTarget, + sessionReplayTarget: CaptureLoggerBridge.SessionReplayTarget, eventsListenerTarget: CaptureLoggerBridge.EventsListenerTarget, appID: String, releaseVersion: String, @@ -34,6 +35,7 @@ final class LoggerBridge: LoggerBridging { sessionStrategy.makeSessionStrategyProvider(), metadataProvider, resourceUtilizationTarget, + sessionReplayTarget, eventsListenerTarget, appID, releaseVersion, @@ -62,6 +64,7 @@ final class LoggerBridge: LoggerBridging { sessionStrategy: SessionStrategy, metadataProvider: CaptureLoggerBridge.MetadataProvider, resourceUtilizationTarget: CaptureLoggerBridge.ResourceUtilizationTarget, + sessionReplayTarget: CaptureLoggerBridge.SessionReplayTarget, eventsListenerTarget: CaptureLoggerBridge.EventsListenerTarget, appID: String, releaseVersion: String, @@ -74,6 +77,7 @@ final class LoggerBridge: LoggerBridging { sessionStrategy: sessionStrategy, metadataProvider: metadataProvider, resourceUtilizationTarget: resourceUtilizationTarget, + sessionReplayTarget: sessionReplayTarget, eventsListenerTarget: eventsListenerTarget, appID: appID, releaseVersion: releaseVersion, @@ -105,8 +109,12 @@ final class LoggerBridge: LoggerBridging { ) } - func logSessionReplay(fields: [CapturePassable.Field], duration: TimeInterval) { - capture_write_session_replay_log(self.loggerID, fields, duration) + func logSessionReplayScreen(fields: [CapturePassable.Field], duration: TimeInterval) { + capture_write_session_replay_screen_log(self.loggerID, fields, duration) + } + + func logSessionReplayScreenshot(fields: [CapturePassable.Field], duration: TimeInterval) { + capture_write_session_replay_screenshot_log(self.loggerID, fields, duration) } func logResourceUtilization(fields: [CapturePassable.Field], duration: TimeInterval) { diff --git a/platform/swift/source/LoggerBridging.swift b/platform/swift/source/LoggerBridging.swift index 654a1c26..1f9c0b0a 100644 --- a/platform/swift/source/LoggerBridging.swift +++ b/platform/swift/source/LoggerBridging.swift @@ -23,7 +23,9 @@ protocol LoggerBridging { blocking: Bool ) - func logSessionReplay(fields: InternalFields, duration: TimeInterval) + func logSessionReplayScreen(fields: InternalFields, duration: TimeInterval) + + func logSessionReplayScreenshot(fields: InternalFields, duration: TimeInterval) func logResourceUtilization(fields: InternalFields, duration: TimeInterval) diff --git a/platform/swift/source/LoggerBridgingFactory.swift b/platform/swift/source/LoggerBridgingFactory.swift index ed55498e..483a480c 100644 --- a/platform/swift/source/LoggerBridgingFactory.swift +++ b/platform/swift/source/LoggerBridgingFactory.swift @@ -14,6 +14,7 @@ final class LoggerBridgingFactory: LoggerBridgingFactoryProvider { sessionStrategy: SessionStrategy, metadataProvider: CaptureLoggerBridge.MetadataProvider, resourceUtilizationTarget: CaptureLoggerBridge.ResourceUtilizationTarget, + sessionReplayTarget: CaptureLoggerBridge.SessionReplayTarget, eventsListenerTarget: CaptureLoggerBridge.EventsListenerTarget, appID: String, releaseVersion: String, @@ -26,6 +27,7 @@ final class LoggerBridgingFactory: LoggerBridgingFactoryProvider { sessionStrategy: sessionStrategy, metadataProvider: metadataProvider, resourceUtilizationTarget: resourceUtilizationTarget, + sessionReplayTarget: sessionReplayTarget, eventsListenerTarget: eventsListenerTarget, appID: appID, releaseVersion: releaseVersion, diff --git a/platform/swift/source/LoggerBridgingFactoryProvider.swift b/platform/swift/source/LoggerBridgingFactoryProvider.swift index 15a1d33b..3dcf9c7c 100644 --- a/platform/swift/source/LoggerBridgingFactoryProvider.swift +++ b/platform/swift/source/LoggerBridgingFactoryProvider.swift @@ -16,6 +16,7 @@ protocol LoggerBridgingFactoryProvider { /// - parameter sessionStrategy: The session strategy to use. /// - parameter metadataProvider: The metadata provider to use. /// - parameter resourceUtilizationTarget: The resource utilization target to use. + /// - parameter sessionReplayTarget: The session replay target to use. /// - parameter eventsListenerTarget: The events listener target to use. /// - parameter appID: The host application application identifier. /// - parameter releaseVersion: The host application release version. @@ -29,6 +30,7 @@ protocol LoggerBridgingFactoryProvider { sessionStrategy: SessionStrategy, metadataProvider: CaptureLoggerBridge.MetadataProvider, resourceUtilizationTarget: CaptureLoggerBridge.ResourceUtilizationTarget, + sessionReplayTarget: CaptureLoggerBridge.SessionReplayTarget, eventsListenerTarget: CaptureLoggerBridge.EventsListenerTarget, appID: String, releaseVersion: String, diff --git a/platform/swift/source/RuntimeVariable.swift b/platform/swift/source/RuntimeVariable.swift index aaa39f31..4627a4ae 100644 --- a/platform/swift/source/RuntimeVariable.swift +++ b/platform/swift/source/RuntimeVariable.swift @@ -39,11 +39,6 @@ extension RuntimeVariable { /// A feature that can be tracked via the runtime configuration system. extension RuntimeVariable { - static let sessionReplay = RuntimeVariable( - name: "client_features.ios.session_replay", - defaultValue: true - ) - static let periodicLowPowerModeReporting = RuntimeVariable( name: "client_features.ios.resource_reporting.low_power", defaultValue: true diff --git a/platform/swift/source/bridging/SessionReplayTarget.swift b/platform/swift/source/bridging/SessionReplayTarget.swift new file mode 100644 index 00000000..611a039e --- /dev/null +++ b/platform/swift/source/bridging/SessionReplayTarget.swift @@ -0,0 +1,20 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +import Foundation + +/// Responsible for emitting session replay logs in response to provided callbacks. +@objc +public protocol SessionReplayTarget { + /// Called to indicate that the target is supposed to prepare and emit a session replay screen log. + func captureScreen() + // Called to indicate that the target should prepare and emit a session replay screenshot log. + // The Rust logger does not request another screenshot until it receives the previously + // requested one. This mechanism is designed to ensure that there are no situations where + // the Rust logger requests screenshots at a rate faster than the platform layer can handle. + func captureScreenshot() +} diff --git a/platform/swift/source/features/Configuration.swift b/platform/swift/source/features/Configuration.swift index 2a54bd98..2715c9c9 100644 --- a/platform/swift/source/features/Configuration.swift +++ b/platform/swift/source/features/Configuration.swift @@ -10,13 +10,12 @@ import Foundation /// A configuration representing the feature set enabled for Capture. public struct Configuration { /// The session replay configuration. - public var sessionReplayConfiguration: SessionReplayConfiguration? + public var sessionReplayConfiguration: SessionReplayConfiguration /// Initializes a new instance of the Capture configuration. /// - /// - parameter sessionReplayConfiguration: The session replay configuration to use. Passing `nil` - /// disables the feature. - public init(sessionReplayConfiguration: SessionReplayConfiguration? = .init()) { + /// - parameter sessionReplayConfiguration: The session replay configuration to use. + public init(sessionReplayConfiguration: SessionReplayConfiguration = .init()) { self.sessionReplayConfiguration = sessionReplayConfiguration } } diff --git a/platform/swift/source/features/SessionReplayConfiguration.swift b/platform/swift/source/features/SessionReplayConfiguration.swift index 0273b012..32a56db4 100644 --- a/platform/swift/source/features/SessionReplayConfiguration.swift +++ b/platform/swift/source/features/SessionReplayConfiguration.swift @@ -9,22 +9,14 @@ import Foundation /// A configuration used to configure Capture session replay feature. public struct SessionReplayConfiguration { - /// The number of seconds between consecutive screen replay captures. - public var captureIntervalSeconds: TimeInterval - /// The closure called at just before session replay starts. Passed `Replay` object can be used to - /// configure the session replay further. - public var willStart: ((Replay) -> Void)? + public var categorizers: [String: AnnotatedView] /// Initializes a new session replay configuration. /// - /// - parameter captureIntervalSeconds: The number of seconds between consecutive screen replay captures. - /// The default is 3s. - /// - parameter willStart: The closure called at just before session replay starts. Passed - /// `Replay` object can be used to configure the session replay - /// further. Passing `nil` means no further configuration. - /// The default is `nil`. - public init(captureIntervalSeconds: TimeInterval = 3, willStart: ((Replay) -> Void)? = nil) { - self.captureIntervalSeconds = captureIntervalSeconds - self.willStart = willStart + /// - parameter categorizers: A mapping that provides additional instructions on how views implemented + /// with specific class names should be represented in session replay + /// capture visualizations. + public init(categorizers: [String: AnnotatedView] = [:]) { + self.categorizers = categorizers } } diff --git a/platform/swift/source/replay/AnnotatedView.swift b/platform/swift/source/replay/AnnotatedView.swift index 7c0fde5f..730100e8 100644 --- a/platform/swift/source/replay/AnnotatedView.swift +++ b/platform/swift/source/replay/AnnotatedView.swift @@ -14,7 +14,7 @@ import UIKit /// 15 types. /// /// Types left: 3 -public enum ViewType: UInt8 { +enum ViewType: UInt8 { case label = 0 case button = 1 case textInput = 2 @@ -53,9 +53,9 @@ public struct AnnotatedView { /// UITextField can add a fragment to represent the entered text. let fragments: [(frame: CGRect, type: ViewType)] - public init(_ type: ViewType, recurse: Bool = true, frame: CGRect = .zero, - fragments: [(CGRect, ViewType)] = [], ignoreWhenEmpty: Bool? = nil, - ignoreChildrenViews: Bool? = nil) + init(_ type: ViewType, recurse: Bool = true, frame: CGRect = .zero, + fragments: [(CGRect, ViewType)] = [], ignoreWhenEmpty: Bool? = nil, + ignoreChildrenViews: Bool? = nil) { self.ignoreWhenEmpty = ignoreWhenEmpty ?? (recurse || type == .ignore) self.ignoreChildrenViews = ignoreChildrenViews ?? diff --git a/platform/swift/source/replay/Replay.swift b/platform/swift/source/replay/Replay.swift index ca0cd8aa..48456457 100644 --- a/platform/swift/source/replay/Replay.swift +++ b/platform/swift/source/replay/Replay.swift @@ -13,7 +13,7 @@ private let kIgnoredWindows = Set(["UIRemoteKeyboardWindow"]) /// Main replay logic. This class can traverse UIWindow(s) as well as serialize view informations into a /// byte array. -public final class Replay { +final class Replay { /// The last known rendering time, expressed in seconds. private(set) var renderTime: CFAbsoluteTime = 0 @@ -32,7 +32,7 @@ public final class Replay { /// /// - parameter type: The annotated view which defines the position and traverse behavior, /// see `AnnotatedViewType` for more information. - public func add(knownClass: String, type: AnnotatedView) { + func add(knownClass: String, type: AnnotatedView) { ReplayCommonCategorizer.knownTypes[knownClass] = type } diff --git a/platform/swift/source/replay/ReplayController.swift b/platform/swift/source/replay/ReplayController.swift deleted file mode 100644 index 3da3524c..00000000 --- a/platform/swift/source/replay/ReplayController.swift +++ /dev/null @@ -1,67 +0,0 @@ -// capture-sdk - bitdrift's client SDK -// Copyright Bitdrift, Inc. All rights reserved. -// -// Use of this source code is governed by a source available license that can be found in the -// LICENSE file or at: -// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt - -@_implementationOnly import CaptureLoggerBridge -@_implementationOnly import CapturePassable -import Foundation - -final class ReplayController { - // Run loop retains the timer while it's being active. - private var timer: QueueTimer? - private let queue = DispatchQueue.serial(withLabelSuffix: "ReplayController", target: .default) - - private let logger: CoreLogging - - init(logger: CoreLogging) { - self.logger = logger - } - - func start(with configuration: SessionReplayConfiguration) { - self.stop() - - guard configuration.captureIntervalSeconds > 0 else { - assertionFailure("session replay capture interval needs to be greater than 0s") - return - } - - let replay = Replay() - configuration.willStart?(replay) - - self.timer = QueueTimer.scheduledTimer( - withTimeInterval: configuration.captureIntervalSeconds, - queue: self.queue - ) { [weak self] in - self?.maybeCaptureScreen(replay: replay) - } - } - - func stop() { - self.timer?.invalidate() - self.timer = nil - } - - // MARK: - Private - - func maybeCaptureScreen(replay: Replay) { - guard self.logger.runtimeValue(.sessionReplay) == true else { - return - } - - DispatchQueue.main.async { [weak self] in - let start = Uptime() - let capturedScreen = replay.capture() - let duration = Uptime().timeIntervalSince(start) - - self?.queue.async { - self?.logger.logSessionReplay( - screen: SessionReplayScreenCapture(data: capturedScreen), - duration: duration - ) - } - } - } -} diff --git a/platform/swift/source/replay/ReplayIdentifiable.swift b/platform/swift/source/replay/ReplayIdentifiable.swift index afecceed..5639d8ab 100644 --- a/platform/swift/source/replay/ReplayIdentifiable.swift +++ b/platform/swift/source/replay/ReplayIdentifiable.swift @@ -22,7 +22,7 @@ import UIKit /// func identify(frame: inout CGRect) -> (type: ViewType, recurse: Bool)? { return (.label, false) } /// } /// ``` -public protocol ReplayIdentifiable where Self: UIView { +protocol ReplayIdentifiable where Self: UIView { /// A function that returns the desired type for the receiver. This method can optionally re-defined /// the final frame. This is useful when the visual content does not match the receiver frame. /// For example, an ImageView with an image that is centered can set the frame for the image itself diff --git a/platform/swift/source/replay/SessionReplayTarget.swift b/platform/swift/source/replay/SessionReplayTarget.swift new file mode 100644 index 00000000..0cc5d908 --- /dev/null +++ b/platform/swift/source/replay/SessionReplayTarget.swift @@ -0,0 +1,54 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +@_implementationOnly import CapturePassable +import Foundation + +final class SessionReplayTarget { + private let queue = DispatchQueue.serial(withLabelSuffix: "ReplayController", target: .default) + private let replay: Replay = Replay() + + var logger: CoreLogging? + + init(configuration: SessionReplayConfiguration) { + for (className, annotatedView) in configuration.categorizers { + self.replay.add(knownClass: className, type: annotatedView) + } + } +} + +extension SessionReplayTarget: CapturePassable.SessionReplayTarget { + func captureScreen() { + DispatchQueue.main.async { [weak self] in + let start = Uptime() + guard let capturedScreen = self?.replay.capture() else { + return + } + + let duration = Uptime().timeIntervalSince(start) + + self?.queue.async { + self?.logger?.logSessionReplayScreen( + screen: SessionReplayCapture(data: capturedScreen), + duration: duration + ) + } + } + } + + func captureScreenshot() { + // TODO: Implement + // DispatchQueue.main.async { [weak self] in + // self?.queue.async { + // self?.logger?.logSessionReplayScreenshot( + // screen: SessionReplayCapture(data: Data()), + // duration: 0 + // ) + // } + // } + } +} diff --git a/platform/swift/source/shared/models/SessionReplayScreenCapture.swift b/platform/swift/source/shared/models/SessionReplayScreenCapture.swift index 5bcbf921..9be61562 100644 --- a/platform/swift/source/shared/models/SessionReplayScreenCapture.swift +++ b/platform/swift/source/shared/models/SessionReplayScreenCapture.swift @@ -8,6 +8,6 @@ import Foundation // Used to log Session Replay Screen Capture data. -struct SessionReplayScreenCapture: Encodable { +struct SessionReplayCapture: Encodable { let data: Data } diff --git a/platform/swift/source/src/bridge.rs b/platform/swift/source/src/bridge.rs index 3e15acf4..06dcd98c 100644 --- a/platform/swift/source/src/bridge.rs +++ b/platform/swift/source/src/bridge.rs @@ -12,7 +12,7 @@ mod bridge_tests; use crate::bridge::ffi::make_nsstring; use crate::ffi::{convert_fields, nsstring_into_string}; use crate::key_value_storage::UserDefaultsStorage; -use crate::{events, ffi, resource_utilization}; +use crate::{events, ffi, resource_utilization, session_replay}; use anyhow::anyhow; use bd_api::{Platform, PlatformNetworkManager, PlatformNetworkStream, StreamEvent}; use bd_client_common::error::{ @@ -436,6 +436,7 @@ extern "C" fn capture_create_logger( session_strategy: *mut Object, provider: *mut Object, resource_utilization_target: *mut Object, + session_replay_target: *mut Object, events_listener_target: *mut Object, app_id: *const c_char, app_version: *const c_char, @@ -499,6 +500,7 @@ extern "C" fn capture_create_logger( resource_utilization_target: Box::new(resource_utilization::Target::new( resource_utilization_target, )), + session_replay_target: Box::new(session_replay::Target::new(session_replay_target)), events_listener_target: Box::new(events::Target::new(events_listener_target)), network: network_manager, platform: Platform::Ios, @@ -623,7 +625,7 @@ extern "C" fn capture_write_log( } #[no_mangle] -extern "C" fn capture_write_session_replay_log( +extern "C" fn capture_write_session_replay_screen_log( logger_id: LoggerId<'_>, fields: *const Object, duration_s: f64, @@ -638,10 +640,33 @@ extern "C" fn capture_write_session_replay_log( }) .collect(); - logger_id.log_session_replay(fields, time::Duration::seconds_f64(duration_s)); + logger_id.log_session_replay_screen(fields, time::Duration::seconds_f64(duration_s)); Ok(()) }, - "swift write session replay log", + "swift write session replay screen log", + ); +} + +#[no_mangle] +extern "C" fn capture_write_session_replay_screenshot_log( + logger_id: LoggerId<'_>, + fields: *const Object, + duration_s: f64, +) { + with_handle_unexpected( + || -> anyhow::Result<()> { + let fields = unsafe { convert_fields(fields) }? + .into_iter() + .map(|field| AnnotatedLogField { + field, + kind: LogFieldKind::Ootb, + }) + .collect(); + + logger_id.log_session_replay_screenshot(fields, time::Duration::seconds_f64(duration_s)); + Ok(()) + }, + "swift write session replay screenshot log", ); } diff --git a/platform/swift/source/src/lib.rs b/platform/swift/source/src/lib.rs index f1dc7b97..2ad6fbaa 100644 --- a/platform/swift/source/src/lib.rs +++ b/platform/swift/source/src/lib.rs @@ -14,3 +14,4 @@ pub mod ffi; pub mod key_value_storage; pub mod resource_utilization; mod session; +pub mod session_replay; diff --git a/platform/swift/source/src/session_replay.rs b/platform/swift/source/src/session_replay.rs new file mode 100644 index 00000000..2ee74cb8 --- /dev/null +++ b/platform/swift/source/src/session_replay.rs @@ -0,0 +1,40 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +use objc::rc::autoreleasepool; +use objc::runtime::Object; + +#[allow(clippy::non_send_fields_in_send_ty)] +pub struct Target { + swift_object: objc::rc::StrongPtr, +} + +unsafe impl Send for Target {} +unsafe impl Sync for Target {} + +impl Target { + #[allow(clippy::not_unsafe_ptr_arg_deref)] + pub fn new(swift_object: *mut Object) -> Self { + Self { + swift_object: unsafe { objc::rc::StrongPtr::retain(swift_object) }, + } + } +} + +impl bd_logger::SessionReplayTarget for Target { + fn capture_screen(&self) { + autoreleasepool(|| { + let () = unsafe { msg_send![*self.swift_object, captureScreen] }; + }); + } + + fn capture_screenshot(&self) { + autoreleasepool(|| { + let () = unsafe { msg_send![*self.swift_object, captureScreenshot] }; + }); + } +} diff --git a/platform/test_helpers/src/lib.rs b/platform/test_helpers/src/lib.rs index 43542ff8..8a25f925 100644 --- a/platform/test_helpers/src/lib.rs +++ b/platform/test_helpers/src/lib.rs @@ -242,6 +242,11 @@ pub extern "C" fn configure_aggressive_continuous_uploads(stream_id: i32) { bd_runtime::runtime::resource_utilization::ResourceUtilizationEnabledFlag::path(), ValueKind::Bool(true), ), + // Enable session replay periodic screen collection. + ( + bd_runtime::runtime::session_replay::PeriodicScreensEnabledFlag::path(), + ValueKind::Bool(true), + ), ], "base".to_string(), )), @@ -457,6 +462,11 @@ pub fn run_resource_utilization_target_tests(target: &dyn bd_logger::ResourceUti target.tick(); } +pub fn run_session_replay_target_tests(target: &dyn bd_logger::SessionReplayTarget) { + target.capture_screen(); + target.capture_screenshot(); +} + pub fn run_events_listener_target_tests(target: &dyn bd_logger::EventsListenerTarget) { target.start(); target.stop(); diff --git a/test/benchmark/src/bin/live_benchmark.rs b/test/benchmark/src/bin/live_benchmark.rs index 5f76f905..b4855181 100644 --- a/test/benchmark/src/bin/live_benchmark.rs +++ b/test/benchmark/src/bin/live_benchmark.rs @@ -47,6 +47,7 @@ fn test_live_match_performance(c: &mut Criterion) { store, metadata_provider, resource_utilization_target: Box::new(bd_test_helpers::resource_utilization::EmptyTarget), + session_replay_target: Box::new(bd_test_helpers::session_replay::NoOpTarget), events_listener_target: Box::new(bd_test_helpers::events::NoOpListenerTarget), device, }) diff --git a/test/benchmark/src/bin/logger_benchmark.rs b/test/benchmark/src/bin/logger_benchmark.rs index 7dee5913..d1c7882f 100644 --- a/test/benchmark/src/bin/logger_benchmark.rs +++ b/test/benchmark/src/bin/logger_benchmark.rs @@ -61,6 +61,7 @@ fn simple_log(c: &mut Criterion) { fields: Vec::new(), }), resource_utilization_target: Box::new(bd_test_helpers::resource_utilization::EmptyTarget), + session_replay_target: Box::new(bd_test_helpers::session_replay::NoOpTarget), events_listener_target: Box::new(bd_test_helpers::events::NoOpListenerTarget), device, store, @@ -105,6 +106,7 @@ fn with_matcher_and_buffer(c: &mut Criterion) { fields: Vec::new(), }), resource_utilization_target: Box::new(bd_test_helpers::resource_utilization::EmptyTarget), + session_replay_target: Box::new(bd_test_helpers::session_replay::NoOpTarget), events_listener_target: Box::new(bd_test_helpers::events::NoOpListenerTarget), device, store, diff --git a/test/platform/jvm/src/lib.rs b/test/platform/jvm/src/lib.rs index 678472b7..6b16879f 100644 --- a/test/platform/jvm/src/lib.rs +++ b/test/platform/jvm/src/lib.rs @@ -17,6 +17,7 @@ use capture::jni::{ErrorReporterHandle, JValueWrapper}; use capture::key_value_storage::PreferencesHandle; use capture::new_global; use capture::resource_utilization::TargetHandler as ResourceUtilizationTargetHandler; +use capture::session_replay::TargetHandler as SessionReplayTargetHandler; use jni::objects::{JClass, JMap, JObject, JString}; use jni::sys::{jint, jlong}; use jni::JNIEnv; @@ -344,6 +345,16 @@ pub extern "C" fn Java_io_bitdrift_capture_CaptureTestJniLibrary_runResourceUtil platform_test_helpers::run_resource_utilization_target_tests(&target); } +#[no_mangle] +pub extern "C" fn Java_io_bitdrift_capture_CaptureTestJniLibrary_runSessionReplayTargetTest( + mut env: JNIEnv<'_>, + _class: JClass<'_>, + target: JObject<'_>, +) { + let target = new_global!(SessionReplayTargetHandler, &mut env, target).unwrap(); + platform_test_helpers::run_session_replay_target_tests(&target); +} + #[no_mangle] pub extern "C" fn Java_io_bitdrift_capture_CaptureTestJniLibrary_runEventsListenerTargetTest( mut env: JNIEnv<'_>, diff --git a/test/platform/swift/bridging/CaptureTestBridge.h b/test/platform/swift/bridging/CaptureTestBridge.h index 634c2381..271463f1 100644 --- a/test/platform/swift/bridging/CaptureTestBridge.h +++ b/test/platform/swift/bridging/CaptureTestBridge.h @@ -56,4 +56,6 @@ void run_key_value_storage_test(); void run_resource_utilization_target_test(id); +void run_session_replay_target_test(id); + void run_events_listener_target_test(id); diff --git a/test/platform/swift/bridging/src/lib.rs b/test/platform/swift/bridging/src/lib.rs index 6eb60fde..0e92fc91 100644 --- a/test/platform/swift/bridging/src/lib.rs +++ b/test/platform/swift/bridging/src/lib.rs @@ -225,6 +225,12 @@ unsafe extern "C" fn run_resource_utilization_target_test(target: *mut Object) { platform_test_helpers::run_resource_utilization_target_tests(&target); } +#[no_mangle] +unsafe extern "C" fn run_session_replay_target_test(target: *mut Object) { + let target = swift_bridge::session_replay::Target::new(target); + platform_test_helpers::run_session_replay_target_tests(&target); +} + #[no_mangle] unsafe extern "C" fn run_events_listener_target_test(target: *mut Object) { let target = swift_bridge::events::Target::new(target); diff --git a/test/platform/swift/unit_integration/core/ConfigurationTests.swift b/test/platform/swift/unit_integration/core/ConfigurationTests.swift index 43b03483..7e15e97a 100644 --- a/test/platform/swift/unit_integration/core/ConfigurationTests.swift +++ b/test/platform/swift/unit_integration/core/ConfigurationTests.swift @@ -27,8 +27,7 @@ final class ConfigurationTests: XCTestCase { func testConfigurationSimple() throws { Logger.start( withAPIKey: "api_key", - sessionStrategy: .fixed(), - configuration: .init(sessionReplayConfiguration: nil) + sessionStrategy: .fixed() ) XCTAssertNotNil(Logger.getShared()) @@ -37,8 +36,7 @@ final class ConfigurationTests: XCTestCase { func testConfigurationDefault() throws { Logger.start( withAPIKey: "api_key", - sessionStrategy: .fixed(), - configuration: .init() + sessionStrategy: .fixed() ) XCTAssertNotNil(Logger.getShared()) diff --git a/test/platform/swift/unit_integration/core/FieldExtensionsTests.swift b/test/platform/swift/unit_integration/core/FieldExtensionsTests.swift index a742f4c9..e417167e 100644 --- a/test/platform/swift/unit_integration/core/FieldExtensionsTests.swift +++ b/test/platform/swift/unit_integration/core/FieldExtensionsTests.swift @@ -14,7 +14,7 @@ final class FieldExtensionTests: XCTestCase { func testSessionReplayCaptureIsEncodedAsData() throws { let field = try Field.make( key: "foo", - value: SessionReplayScreenCapture(data: try XCTUnwrap("test".data(using: .utf8))) + value: SessionReplayCapture(data: try XCTUnwrap("test".data(using: .utf8))) ) XCTAssert(field.data is Data) diff --git a/test/platform/swift/unit_integration/core/Logger+Tests.swift b/test/platform/swift/unit_integration/core/Logger+Tests.swift index 5ca00542..bb440ad7 100644 --- a/test/platform/swift/unit_integration/core/Logger+Tests.swift +++ b/test/platform/swift/unit_integration/core/Logger+Tests.swift @@ -17,7 +17,7 @@ extension Logger { sessionStrategy: SessionStrategy = .fixed(), dateProvider: DateProvider? = nil, fieldProviders: [FieldProvider] = [], - configuration: Configuration, + configuration: Configuration = .init(), loggerBridgingFactoryProvider: LoggerBridgingFactoryProvider = LoggerBridgingFactory() ) throws -> Logger { diff --git a/test/platform/swift/unit_integration/core/LoggerTests.swift b/test/platform/swift/unit_integration/core/LoggerTests.swift index ba609443..6f58cfe2 100644 --- a/test/platform/swift/unit_integration/core/LoggerTests.swift +++ b/test/platform/swift/unit_integration/core/LoggerTests.swift @@ -15,8 +15,7 @@ final class LoggerTests: XCTestCase { func testPropertiesReturnsCorrectValues() throws { let logger = try Logger.testLogger( withAPIKey: "test_api_key", - bufferDirectory: Logger.tempBufferDirectory(), - configuration: .init(sessionReplayConfiguration: nil) + bufferDirectory: Logger.tempBufferDirectory() ) XCTAssertEqual(36, logger.sessionID.count) @@ -27,8 +26,7 @@ final class LoggerTests: XCTestCase { func testLogger() throws { let logger = try Logger.testLogger( withAPIKey: "test_api_key", - bufferDirectory: Logger.tempBufferDirectory(), - configuration: .init(sessionReplayConfiguration: nil) + bufferDirectory: Logger.tempBufferDirectory() ) logger.log(level: .debug, message: "test with fields", fields: ["hello": "world"], type: .normal) @@ -53,8 +51,7 @@ final class LoggerTests: XCTestCase { logger = try Logger.testLogger( withAPIKey: "test_api_key", bufferDirectory: Logger.tempBufferDirectory(), - fieldProviders: [fieldProvider], - configuration: .init(sessionReplayConfiguration: nil) + fieldProviders: [fieldProvider] ) XCTAssertEqual(.completed, XCTWaiter().wait(for: [expectation], timeout: 1)) @@ -99,8 +96,7 @@ final class LoggerTests: XCTestCase { bufferDirectory: Logger.tempBufferDirectory(), sessionStrategy: SessionStrategy.fixed(), dateProvider: dateProvider, - fieldProviders: [fieldProvider], - configuration: .init(sessionReplayConfiguration: nil) + fieldProviders: [fieldProvider] ) let expectations = [ @@ -148,7 +144,6 @@ final class LoggerTests: XCTestCase { let logger = try Logger.testLogger( withAPIKey: "test_api_key", bufferDirectory: Logger.tempBufferDirectory(), - configuration: .init(sessionReplayConfiguration: .init(captureIntervalSeconds: 5)), loggerBridgingFactoryProvider: MockLoggerBridgingFactory(logger: bridge) ) @@ -196,7 +191,6 @@ final class LoggerTests: XCTestCase { let logger = try Logger.testLogger( withAPIKey: "test_api_key", bufferDirectory: Logger.tempBufferDirectory(), - configuration: .init(sessionReplayConfiguration: .init(captureIntervalSeconds: 5)), loggerBridgingFactoryProvider: MockLoggerBridgingFactory(logger: bridge) ) logger.log(requestInfo) @@ -246,7 +240,6 @@ final class LoggerTests: XCTestCase { let logger = try Logger.testLogger( withAPIKey: "test_api_key", bufferDirectory: Logger.tempBufferDirectory(), - configuration: .init(sessionReplayConfiguration: .init(captureIntervalSeconds: 5)), loggerBridgingFactoryProvider: MockLoggerBridgingFactory(logger: bridge) ) logger.log(responseInfo) diff --git a/test/platform/swift/unit_integration/core/bridge/SessionReplayTargetTests.swift b/test/platform/swift/unit_integration/core/bridge/SessionReplayTargetTests.swift new file mode 100644 index 00000000..75af888b --- /dev/null +++ b/test/platform/swift/unit_integration/core/bridge/SessionReplayTargetTests.swift @@ -0,0 +1,38 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +@testable import Capture +import CaptureMocks +@testable import CaptureTestBridge +import XCTest + +final class SessionReplayTargetTests: XCTestCase { + private var target: Capture.SessionReplayTarget! + private var logger: MockCoreLogging! + + override func setUp() { + super.setUp() + + self.logger = MockCoreLogging() + + self.target = SessionReplayTarget(configuration: .init()) + self.target.logger = self.logger + } + + func testSessionReplayTargetDoesNotCrash() { + run_session_replay_target_test(self.target) + } + + func testEmitsSessionReplayScreenLog() { + let expectation = self.expectation(description: "screen log is emitted") + + self.logger.logSessionReplayScreen = expectation + self.target.captureScreen() + + XCTAssertEqual(.completed, XCTWaiter().wait(for: [expectation], timeout: 0.5)) + } +} diff --git a/test/platform/swift/unit_integration/core/network/LoggerE2ETest.swift b/test/platform/swift/unit_integration/core/network/LoggerE2ETest.swift index ca35c901..d2a91962 100644 --- a/test/platform/swift/unit_integration/core/network/LoggerE2ETest.swift +++ b/test/platform/swift/unit_integration/core/network/LoggerE2ETest.swift @@ -61,7 +61,7 @@ final class CaptureE2ENetworkTests: BaseNetworkingTestCase { var logger: Logger! var storage: StorageProvider! - private func setUpLogger(configuration: Configuration) throws -> Logger { + private func setUpLogger() throws -> Logger { self.storage = MockStorageProvider() let apiURL = self.setUpTestServer() @@ -71,7 +71,7 @@ final class CaptureE2ENetworkTests: BaseNetworkingTestCase { bufferDirectory: self.setUpSDKDirectory(), apiURL: apiURL, remoteErrorReporter: MockRemoteErrorReporter(), - configuration: configuration, + configuration: .init(), sessionStrategy: SessionStrategy.fixed(sessionIDGenerator: { "mock-group-id" }), dateProvider: MockDateProvider(), fieldProviders: [ @@ -95,12 +95,7 @@ final class CaptureE2ENetworkTests: BaseNetworkingTestCase { } func testSessionReplay() async throws { - _ = try self.setUpLogger( - configuration: .init( - // Configure session replay to fire quickly. - sessionReplayConfiguration: .init(captureIntervalSeconds: 1) - ) - ) + _ = try self.setUpLogger() let streamID = try await nextApiStream() configure_aggressive_continuous_uploads(streamID) @@ -139,9 +134,7 @@ final class CaptureE2ENetworkTests: BaseNetworkingTestCase { deviceAttributes.start() networkAttributes.start(with: MockCoreLogging()) - let logger = try self.setUpLogger( - configuration: .init(sessionReplayConfiguration: .init(captureIntervalSeconds: 10)) - ) + let logger = try self.setUpLogger() // Add a valid field, it should be present. logger.addField(withKey: "foo", value: "value_foo") @@ -213,7 +206,7 @@ final class CaptureE2ENetworkTests: BaseNetworkingTestCase { let fields: [String: Encodable] = [ "field": "passed_in", "screen_replay": try XCTUnwrap( - SessionReplayScreenCapture(data: try XCTUnwrap( + SessionReplayCapture(data: try XCTUnwrap( "hello".data(using: .utf8) )) ), diff --git a/test/platform/swift/unit_integration/core/network/helper/BaseNetworkingTestCase.swift b/test/platform/swift/unit_integration/core/network/helper/BaseNetworkingTestCase.swift index c41db19e..0293a9cf 100644 --- a/test/platform/swift/unit_integration/core/network/helper/BaseNetworkingTestCase.swift +++ b/test/platform/swift/unit_integration/core/network/helper/BaseNetworkingTestCase.swift @@ -20,7 +20,7 @@ open class BaseNetworkingTestCase: XCTestCase { private(set) var sdkDirectory: URL? private(set) var testServerStarted = false - private class MockMetadataProvider: CaptureLoggerBridge.MetadataProvider { + private final class MockMetadataProvider: CaptureLoggerBridge.MetadataProvider { func timestamp() -> TimeInterval { // Matches "2022-10-26T17:56:41.520058155Z" when formatted. return Date(timeIntervalSince1970: 1_666_807_001.52005815).timeIntervalSince1970 @@ -35,10 +35,15 @@ open class BaseNetworkingTestCase: XCTestCase { } } - private class MockResourceUtilizationTarget: CaptureLoggerBridge.ResourceUtilizationTarget { + private final class MockResourceUtilizationTarget: CaptureLoggerBridge.ResourceUtilizationTarget { func tick() {} } + private final class MockSessionReplayTarget: CaptureLoggerBridge.SessionReplayTarget { + func captureScreen() {} + func captureScreenshot() {} + } + // swiftlint:disable:next test_case_accessibility func setUpSDKDirectory() -> URL { let testUUID = UUID().uuidString @@ -76,6 +81,7 @@ open class BaseNetworkingTestCase: XCTestCase { sessionStrategy: .fixed(), metadataProvider: MockMetadataProvider(), resourceUtilizationTarget: MockResourceUtilizationTarget(), + sessionReplayTarget: MockSessionReplayTarget(), eventsListenerTarget: MockEventsListenerTarget(), appID: "io.bitdrift.capture.test", releaseVersion: "", diff --git a/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift b/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift index 35f14618..34a95214 100644 --- a/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift +++ b/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift @@ -29,6 +29,11 @@ public final class MockCoreLogging { public let duration: TimeInterval } + public struct SessionReplayScreenLog { + public let screen: SessionReplayCapture + public let duration: TimeInterval + } + public private(set) var logs = [Log]() public var logExpectation: XCTestExpectation? @@ -38,6 +43,9 @@ public final class MockCoreLogging { public private(set) var resourceUtilizationLogs = [ResourceUtilizationLog]() public var logResourceUtilizationExpectation: XCTestExpectation? + public private(set) var sessionReplayScreenLogs = [SessionReplayScreenLog]() + public var logSessionReplayScreen: XCTestExpectation? + public var shouldLogAppUpdateEvent = false public private(set) var mockedRuntimeVariables = [String: Any]() @@ -103,7 +111,14 @@ extension MockCoreLogging: CoreLogging { self.logExpectation?.fulfill() } - public func logSessionReplay(screen _: SessionReplayScreenCapture, duration _: TimeInterval) {} + public func logSessionReplayScreen(screen: SessionReplayCapture, duration: TimeInterval) { + self.sessionReplayScreenLogs.append(SessionReplayScreenLog( + screen: screen, duration: duration) + ) + self.logSessionReplayScreen?.fulfill() + } + + public func logSessionReplayScreenshot(screen _: SessionReplayCapture, duration _: TimeInterval) {} public func logResourceUtilization(fields: Fields, duration: TimeInterval) { self.resourceUtilizationLogs.append(ResourceUtilizationLog(fields: fields, duration: duration)) diff --git a/test/platform/swift/unit_integration/mocks/MockLoggerBridging.swift b/test/platform/swift/unit_integration/mocks/MockLoggerBridging.swift index 68f97bd0..84cb66cf 100644 --- a/test/platform/swift/unit_integration/mocks/MockLoggerBridging.swift +++ b/test/platform/swift/unit_integration/mocks/MockLoggerBridging.swift @@ -77,7 +77,9 @@ extension MockLoggerBridging: LoggerBridging { } } - public func logSessionReplay(fields _: [Field], duration _: TimeInterval) {} + public func logSessionReplayScreen(fields _: [Field], duration _: TimeInterval) {} + + public func logSessionReplayScreenshot(fields _: [Field], duration _: TimeInterval) {} public func logResourceUtilization(fields _: [Field], duration _: TimeInterval) {} diff --git a/test/platform/swift/unit_integration/mocks/MockLoggerBridgingFactory.swift b/test/platform/swift/unit_integration/mocks/MockLoggerBridgingFactory.swift index 9b681889..8861c4da 100644 --- a/test/platform/swift/unit_integration/mocks/MockLoggerBridgingFactory.swift +++ b/test/platform/swift/unit_integration/mocks/MockLoggerBridgingFactory.swift @@ -23,6 +23,7 @@ public final class MockLoggerBridgingFactory: LoggerBridgingFactoryProvider { sessionStrategy _: SessionStrategy, metadataProvider _: CaptureLoggerBridge.MetadataProvider, resourceUtilizationTarget _: CaptureLoggerBridge.ResourceUtilizationTarget, + sessionReplayTarget _: CaptureLoggerBridge.SessionReplayTarget, eventsListenerTarget _: CaptureLoggerBridge.EventsListenerTarget, appID _: String, releaseVersion _: String,