Skip to content

Commit b84ec43

Browse files
authored
feat: add runtime context properties (#136)
* feat: add runtime context properties * address pr review feedback
1 parent dba50e1 commit b84ec43

8 files changed

Lines changed: 337 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
cargo/posthog-rs: minor
3+
---
4+
5+
Add OS runtime context properties to captured events.

Cargo.lock

Lines changed: 207 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ serde_json = "1.0.64"
2121
semver = "1.0.24"
2222
derive_builder = "0.20.2"
2323
uuid = { version = "1.13.2", features = ["serde", "v7"] }
24+
os_info = "3.14"
2425
sha1 = "0.10"
2526
regex = "1.10"
2627
tracing = "0.1"

src/client/common.rs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::collections::{HashMap, HashSet};
2-
use std::sync::Mutex;
2+
use std::sync::{Mutex, OnceLock};
33

44
use crate::client::BeforeSendHook;
55
use crate::client::CaptureDefaults;
@@ -14,6 +14,32 @@ pub(super) const MAX_FLAG_CALLED_CACHE_SIZE: usize = 50_000;
1414

1515
pub(super) type FlagEventDedupCache = Mutex<HashMap<String, HashSet<String>>>;
1616

17+
struct RuntimeContext {
18+
os: String,
19+
os_version: String,
20+
}
21+
22+
static RUNTIME_CONTEXT: OnceLock<RuntimeContext> = OnceLock::new();
23+
24+
fn runtime_context() -> &'static RuntimeContext {
25+
RUNTIME_CONTEXT.get_or_init(|| {
26+
let info = os_info::get();
27+
RuntimeContext {
28+
os: info.os_type().to_string(),
29+
os_version: info.version().to_string(),
30+
}
31+
})
32+
}
33+
34+
pub(super) fn apply_runtime_context(event: &mut Event) {
35+
let context = runtime_context();
36+
event.insert_prop_default("$os", serde_json::Value::String(context.os.clone()));
37+
event.insert_prop_default(
38+
"$os_version",
39+
serde_json::Value::String(context.os_version.clone()),
40+
);
41+
}
42+
1743
pub(super) fn flag_event_dedup_cache() -> FlagEventDedupCache {
1844
Mutex::new(HashMap::new())
1945
}
@@ -326,6 +352,32 @@ mod tests {
326352
assert_eq!(event.properties().get("$geoip_disable"), Some(&json!(true)));
327353
}
328354

355+
#[test]
356+
fn runtime_context_adds_missing_os_properties_only() {
357+
let mut event = Event::new("test", "user-1");
358+
event.insert_prop("$os", "custom-os").unwrap();
359+
360+
apply_runtime_context(&mut event);
361+
362+
assert_eq!(event.properties().get("$os"), Some(&json!("custom-os")));
363+
assert!(event.properties().contains_key("$os_version"));
364+
assert!(!event.properties().contains_key("$os_arch"));
365+
}
366+
367+
#[test]
368+
fn flag_called_event_leaves_runtime_context_to_capture_path() {
369+
let event = flag_called_event(
370+
flag_params(HashMap::new(), HashMap::new(), None),
371+
false,
372+
true,
373+
)
374+
.expect("valid flag-called event");
375+
376+
assert!(!event.properties().contains_key("$os"));
377+
assert!(!event.properties().contains_key("$os_version"));
378+
assert!(!event.properties().contains_key("$os_arch"));
379+
}
380+
329381
#[test]
330382
fn before_send_hooks_mutate_and_drop_events() {
331383
let options = crate::ClientOptionsBuilder::default()

src/client/v0_capture.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use reqwest::blocking::RequestBuilder;
88
use reqwest::RequestBuilder;
99

1010
use super::{
11-
common::{apply_before_send_hooks, apply_capture_defaults},
11+
common::{apply_before_send_hooks, apply_capture_defaults, apply_runtime_context},
1212
BeforeSendHook, CaptureDefaults, ClientOptions,
1313
};
1414
use crate::error::Error;
@@ -24,6 +24,7 @@ use crate::event::{BatchRequest, Event, InnerEvent};
2424
/// event before calling `capture()` keeps their value.
2525
pub(crate) fn prepare_event(event: &mut Event, defaults: &CaptureDefaults) {
2626
apply_capture_defaults(event, defaults);
27+
apply_runtime_context(event);
2728
event.prepare_for_v0();
2829
}
2930

src/client/v1_capture.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use super::retry::{backoff_duration, is_retryable_status};
1414
// Re-exported so the V1 capture loops in the client modules can reach them as
1515
// `v1_capture::parse_retry_after` / `v1_capture::Step`.
1616
pub(crate) use super::retry::{parse_retry_after, Step};
17-
use super::{CaptureCompression, CaptureDefaults, ClientOptions};
17+
use super::{common::apply_runtime_context, CaptureCompression, CaptureDefaults, ClientOptions};
1818
use crate::error::Error;
1919
use crate::event::Event;
2020
use crate::event_v1::{CaptureResponse, EventResult, EventStatus, V1ErrorResponse, V1Event};
@@ -27,7 +27,9 @@ pub(crate) fn build_events(events: &[Event], defaults: &CaptureDefaults) -> Vec<
2727
events
2828
.iter()
2929
.map(|event| {
30-
let mut v1 = V1Event::from_event(event);
30+
let mut event = event.clone();
31+
apply_runtime_context(&mut event);
32+
let mut v1 = V1Event::from_event(&event);
3133
if let serde_json::Value::Object(ref mut map) = v1.properties {
3234
if defaults.disable_geoip {
3335
map.entry("$geoip_disable")

0 commit comments

Comments
 (0)