forked from 1b-agent/fieldcraft-upstream
App: send usage batches to PostHog Cloud EU from a background thread (#238)
ureq over rustls (ring) with the OS certificate verifier. The project key is the build-time FIELDCRAFT_POSTHOG_KEY; without it nothing is asked or sent. FIELDCRAFT_TELEMETRY=0 turns it off; --control and CI runs only with FIELDCRAFT_TELEMETRY=1. A failed post drops its batch. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
79d583ea0f
commit
ed9ec7748d
5 files changed
+1586
-34
No files matched your search
Generated
+232
-10
@@ -431,6 +431,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.10.0"
|
||||
@@ -731,6 +737,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@@ -744,7 +760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -757,7 +773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1170,7 +1186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1245,9 +1261,12 @@ dependencies = [
|
||||
"fieldcraft-engine",
|
||||
"fieldcraft-io",
|
||||
"fieldcraft-ui-egui",
|
||||
"getrandom 0.3.4",
|
||||
"log",
|
||||
"rfd",
|
||||
"rustls",
|
||||
"serde_json",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1446,7 +1465,7 @@ dependencies = [
|
||||
name = "fieldcraft-web"
|
||||
version = "0.5.8"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"eframe",
|
||||
"egui",
|
||||
"fieldcraft-engine",
|
||||
@@ -1821,6 +1840,22 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.15"
|
||||
@@ -2628,6 +2663,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.55"
|
||||
@@ -3149,6 +3190,20 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "robust"
|
||||
version = "1.2.0"
|
||||
@@ -3208,7 +3263,81 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3226,6 +3355,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
@@ -3238,6 +3376,29 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "1.3.0"
|
||||
@@ -3626,6 +3787,12 @@ dependencies = [
|
||||
"float-cmp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "svgtypes"
|
||||
version = "0.16.1"
|
||||
@@ -3678,7 +3845,7 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix 1.1.5",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4033,7 +4200,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4060,6 +4227,40 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"ureq-proto",
|
||||
"utf8-zero",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "usvg"
|
||||
version = "0.48.1"
|
||||
@@ -4079,6 +4280,12 @@ dependencies = [
|
||||
"tiny-skia-path",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8-zero"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.28.0"
|
||||
@@ -4379,6 +4586,15 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
@@ -4574,7 +4790,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4867,7 +5083,7 @@ dependencies = [
|
||||
"calloop 0.13.0",
|
||||
"cfg_aliases",
|
||||
"concurrent-queue",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics",
|
||||
"cursor-icon",
|
||||
"dpi",
|
||||
@@ -4919,7 +5135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5124,6 +5340,12 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "3.0.0"
|
||||
|
||||
+1215
-22
File diff suppressed because it is too large.
Load diff
@@ -18,6 +18,11 @@ eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "
|
||||
egui = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
# Usage analytics (docs/privacy.md): an HTTPS POST with rustls on ring, checked against the
|
||||
# operating system's certificates (no bundled root list), and a random install id.
|
||||
ureq = { version = "3.4", default-features = false, features = ["rustls-no-provider", "platform-verifier"] }
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
||||
getrandom = "0.3"
|
||||
|
||||
[build-dependencies]
|
||||
# The Windows executable's icon resource (build.rs).
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod control_server;
|
||||
mod usage;
|
||||
|
||||
use fieldcraft_engine::autosave::{self, Autosaver};
|
||||
use fieldcraft_engine::{Session, crash, logfile};
|
||||
@@ -49,6 +50,8 @@ struct App {
|
||||
autosaved_at: f64,
|
||||
/// The first frame was drawn (the crash journal says the app is up).
|
||||
running: bool,
|
||||
/// Sends the anonymous usage events, when this run may (docs/privacy.md).
|
||||
usage: Option<usage::Sender>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -82,6 +85,10 @@ impl eframe::App for App {
|
||||
{
|
||||
log::warn!("autosave: {e}");
|
||||
}
|
||||
self.app.usage_closing();
|
||||
if let Some(u) = self.usage.take() {
|
||||
u.finish();
|
||||
}
|
||||
log::info!("FieldCraft closed");
|
||||
crash::end_session();
|
||||
}
|
||||
@@ -103,7 +110,8 @@ fn services() -> Services {
|
||||
})),
|
||||
// Beside the recovery folder in the per-user data folder.
|
||||
recent_path: fieldcraft_io::recovery::default_dir().and_then(|d| d.parent().map(|p| p.join("recent.json"))),
|
||||
prefs_path: fieldcraft_io::recovery::default_dir().and_then(|d| d.parent().map(|p| p.join("prefs.json"))),
|
||||
prefs: fieldcraft_io::recovery::default_dir()
|
||||
.and_then(|d| d.parent().map(|p| fieldcraft_ui_egui::files::PrefsStore::file(p.join("prefs.json")))),
|
||||
open_url: Some(Box::new(open_url)),
|
||||
}
|
||||
}
|
||||
@@ -236,6 +244,14 @@ fn main() -> eframe::Result {
|
||||
// The window's model: its commands go into the crash journal.
|
||||
session.crash_journal = crash::enabled();
|
||||
let mut app = ModelApp::new(session).with_services(services());
|
||||
// Anonymous usage: asked once, recorded only when the user agreed (docs/privacy.md).
|
||||
let usage = match usage::host(args.control.is_some()) {
|
||||
Some(host) => {
|
||||
app = app.with_usage(host);
|
||||
usage::start()
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
app.custom_titlebar = CUSTOM_TITLEBAR;
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
@@ -261,7 +277,7 @@ fn main() -> eframe::Result {
|
||||
Autosaver::new(&dir).map_err(|e| log::warn!("autosave unavailable: {e}")).ok()
|
||||
});
|
||||
app.announce_crashes(&crashed);
|
||||
Ok(Box::new(App { app, autosaver, autosaved_at: 0.0, running: false }))
|
||||
Ok(Box::new(App { app, autosaver, autosaved_at: 0.0, running: false, usage }))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
//! Sending the anonymous usage events (docs/privacy.md) to PostHog Cloud EU: a background
|
||||
//! thread posts the queued events ([`fieldcraft_engine::usage`]) in batches every half minute and
|
||||
//! when the window closes. A failed post (offline, a slow network) drops its batch silently;
|
||||
//! nothing waits on it but the thread.
|
||||
//!
|
||||
//! The project key is a build setting, `FIELDCRAFT_POSTHOG_KEY`, which only the release workflow
|
||||
//! sets: local, development and fork builds have none and send nothing.
|
||||
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use fieldcraft_engine::usage;
|
||||
use fieldcraft_ui_egui::privacy::UsageHost;
|
||||
|
||||
/// The PostHog project key the release build was made with.
|
||||
const KEY: Option<&str> = option_env!("FIELDCRAFT_POSTHOG_KEY");
|
||||
/// How often queued events are sent.
|
||||
const INTERVAL: Duration = Duration::from_secs(30);
|
||||
/// The longest one post may take.
|
||||
const TIMEOUT: Duration = Duration::from_secs(10);
|
||||
/// How long closing the window waits for the last batch.
|
||||
const LAST_WAIT: Duration = Duration::from_secs(2);
|
||||
|
||||
fn key() -> Option<&'static str> {
|
||||
KEY.map(str::trim).filter(|k| !k.is_empty())
|
||||
}
|
||||
|
||||
/// What the window needs to ask and record, when this run may share usage: the build has a key,
|
||||
/// `FIELDCRAFT_TELEMETRY` is not `0`, and neither CI nor `--control` automation runs it (unless
|
||||
/// `FIELDCRAFT_TELEMETRY=1`).
|
||||
pub fn host(automation: bool) -> Option<UsageHost> {
|
||||
let telemetry = std::env::var("FIELDCRAFT_TELEMETRY").ok();
|
||||
let ci = std::env::var_os("CI").is_some_and(|v| !v.is_empty());
|
||||
usage::allowed(key().is_some(), telemetry.as_deref(), automation, ci).then(|| UsageHost {
|
||||
platform: "desktop",
|
||||
os: std::env::consts::OS,
|
||||
arch: std::env::consts::ARCH,
|
||||
new_id: Box::new(new_id),
|
||||
})
|
||||
}
|
||||
|
||||
/// A random (version 4) UUID from the operating system's random source.
|
||||
fn new_id() -> Option<String> {
|
||||
let mut b = [0u8; 16];
|
||||
getrandom::fill(&mut b).ok()?;
|
||||
b[6] = (b[6] & 0x0f) | 0x40;
|
||||
b[8] = (b[8] & 0x3f) | 0x80;
|
||||
let hex: String = b.iter().map(|x| format!("{x:02x}")).collect();
|
||||
Some(format!("{}-{}-{}-{}-{}", &hex[..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..]))
|
||||
}
|
||||
|
||||
/// The sending thread; [`Sender::finish`] sends what is left and stops it.
|
||||
pub struct Sender {
|
||||
stop: mpsc::Sender<()>,
|
||||
finished: mpsc::Receiver<()>,
|
||||
}
|
||||
|
||||
/// Start the sending thread (only when the build has a key). `FIELDCRAFT_TELEMETRY_HOST`
|
||||
/// replaces PostHog's host, for checking the payload against a local listener.
|
||||
pub fn start() -> Option<Sender> {
|
||||
let key = key()?;
|
||||
let host = std::env::var("FIELDCRAFT_TELEMETRY_HOST").ok().filter(|h| !h.trim().is_empty()).unwrap_or_else(|| usage::DEFAULT_HOST.into());
|
||||
let url = format!("{}/batch/", host.trim_end_matches('/'));
|
||||
// rustls's process-wide crypto (ring); another part of the app may have installed it first.
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
let agent: ureq::Agent = ureq::Agent::config_builder()
|
||||
.timeout_global(Some(TIMEOUT))
|
||||
.tls_config(ureq::tls::TlsConfig::builder().root_certs(ureq::tls::RootCerts::PlatformVerifier).build())
|
||||
.build()
|
||||
.into();
|
||||
let (stop, stopped) = mpsc::channel::<()>();
|
||||
let (done, finished) = mpsc::channel::<()>();
|
||||
let spawned = std::thread::Builder::new().name("fieldcraft usage".into()).spawn(move || {
|
||||
loop {
|
||||
let last = !matches!(stopped.recv_timeout(INTERVAL), Err(mpsc::RecvTimeoutError::Timeout));
|
||||
while let Some(batch) = usage::take_batch() {
|
||||
if let Err(e) = agent.post(&url).header("Content-Type", "application/json").send(batch.body(key)) {
|
||||
// Offline or refused: this batch is dropped, the rest waits for the next try.
|
||||
log::debug!("usage: {} event(s) not sent: {e}", batch.events.len());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if last {
|
||||
let _ = done.send(());
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
match spawned {
|
||||
Ok(_) => Some(Sender { stop, finished }),
|
||||
Err(e) => {
|
||||
log::warn!("usage: no sending thread: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sender {
|
||||
/// Send what is queued (waiting briefly) and stop.
|
||||
pub fn finish(self) {
|
||||
let _ = self.stop.send(());
|
||||
let _ = self.finished.recv_timeout(LAST_WAIT);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn install_ids_are_random_version_4_uuids() {
|
||||
let (a, b) = (super::new_id().unwrap(), super::new_id().unwrap());
|
||||
assert_ne!(a, b);
|
||||
assert_eq!(a.len(), 36);
|
||||
assert_eq!(a.as_bytes()[14], b'4');
|
||||
assert!(matches!(a.as_bytes()[19], b'8' | b'9' | b'a' | b'b'), "{a}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user