Skip to main content

worker/
tail.rs

1use crate::{env::EnvBinding, Result};
2use serde::{Deserialize, Serialize};
3use serde_json::Value as JsonValue;
4use wasm_bindgen::{JsCast, JsValue};
5use worker_sys::TailEvent as TailEventSys;
6
7#[derive(Debug, Clone)]
8pub struct TailEvent(TailEventSys);
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub enum TailEventKind {
13    Outcome,
14    SpanOpen,
15    SpanClose,
16    DiagnosticChannel,
17    Exception,
18    Log,
19    Return,
20    Attributes,
21    Unknown(String),
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct TypedTailEvent {
26    pub kind: TailEventKind,
27    pub raw: JsonValue,
28}
29
30unsafe impl Send for TailEvent {}
31unsafe impl Sync for TailEvent {}
32
33impl TailEvent {
34    pub fn invocation_id(&self) -> Option<String> {
35        self.0.invocation_id()
36    }
37
38    pub fn span_context(&self) -> JsValue {
39        self.0.span_context()
40    }
41
42    pub fn timestamp(&self) -> Option<f64> {
43        self.0.timestamp()
44    }
45
46    pub fn sequence(&self) -> Option<u64> {
47        sequence_from_js(self.0.sequence())
48    }
49
50    pub fn event_raw(&self) -> JsValue {
51        self.0.event()
52    }
53
54    pub fn event_typed(&self) -> Result<TypedTailEvent> {
55        let raw: JsonValue = serde_wasm_bindgen::from_value(self.0.event())?;
56        Ok(parse_typed_tail_event(raw))
57    }
58}
59
60fn sequence_from_js(sequence: Option<f64>) -> Option<u64> {
61    sequence.map(|value| value.round() as u64)
62}
63
64fn parse_typed_tail_event(raw: JsonValue) -> TypedTailEvent {
65    let kind = raw
66        .get("type")
67        .and_then(JsonValue::as_str)
68        .map(parse_tail_event_kind)
69        .unwrap_or_else(|| TailEventKind::Unknown("unknown".to_string()));
70    TypedTailEvent { kind, raw }
71}
72
73fn parse_tail_event_kind(raw_type: &str) -> TailEventKind {
74    match raw_type {
75        "outcome" => TailEventKind::Outcome,
76        "spanOpen" => TailEventKind::SpanOpen,
77        "spanClose" => TailEventKind::SpanClose,
78        "diagnosticChannel" => TailEventKind::DiagnosticChannel,
79        "exception" => TailEventKind::Exception,
80        "log" => TailEventKind::Log,
81        "return" => TailEventKind::Return,
82        "attributes" => TailEventKind::Attributes,
83        other => TailEventKind::Unknown(other.to_string()),
84    }
85}
86
87impl From<TailEventSys> for TailEvent {
88    fn from(value: TailEventSys) -> Self {
89        Self(value)
90    }
91}
92
93impl EnvBinding for TailEvent {
94    const TYPE_NAME: &'static str = "Object";
95
96    fn get(val: JsValue) -> Result<Self> {
97        if !val.is_object() {
98            return Err("Binding cannot be cast to TailEvent from non-object value".into());
99        }
100
101        let has_event = js_sys::Reflect::has(&val, &JsValue::from("event"))?;
102        if !has_event {
103            return Err("Binding cannot be cast to TailEvent: missing `event` field".into());
104        }
105
106        Ok(Self(val.unchecked_into()))
107    }
108}
109
110impl AsRef<JsValue> for TailEvent {
111    fn as_ref(&self) -> &JsValue {
112        &self.0
113    }
114}
115
116impl JsCast for TailEvent {
117    fn instanceof(val: &JsValue) -> bool {
118        val.is_object()
119    }
120
121    fn unchecked_from_js(val: JsValue) -> Self {
122        Self(val.unchecked_into())
123    }
124
125    fn unchecked_from_js_ref(val: &JsValue) -> &Self {
126        unsafe { &*(val as *const JsValue as *const Self) }
127    }
128}
129
130impl From<TailEvent> for JsValue {
131    fn from(value: TailEvent) -> Self {
132        value.0.into()
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::{parse_typed_tail_event, sequence_from_js, TailEventKind};
139
140    #[test]
141    fn known_tail_variant_is_parsed() {
142        let parsed = parse_typed_tail_event(serde_json::json!({
143            "type": "log",
144            "message": "hello"
145        }));
146        assert_eq!(parsed.kind, TailEventKind::Log);
147    }
148
149    #[test]
150    fn unknown_tail_variant_falls_back_safely() {
151        let parsed = parse_typed_tail_event(serde_json::json!({
152            "type": "newFutureType",
153            "payload": { "x": 1 }
154        }));
155        assert_eq!(
156            parsed.kind,
157            TailEventKind::Unknown("newFutureType".to_string())
158        );
159    }
160
161    #[test]
162    fn sequence_conversion_rounds_from_js_number() {
163        assert_eq!(sequence_from_js(Some(10.2)), Some(10));
164        assert_eq!(sequence_from_js(Some(10.7)), Some(11));
165        assert_eq!(sequence_from_js(None), None);
166    }
167}