diff --git a/crates/cheers/src/router/assets/track-plugin.ts b/crates/cheers/src/router/assets/track-plugin.ts index 1657eca..8f855f1 100644 --- a/crates/cheers/src/router/assets/track-plugin.ts +++ b/crates/cheers/src/router/assets/track-plugin.ts @@ -21,6 +21,7 @@ type PageViewItem = { context: TrackContext; referrer?: string; navigation_type?: NavigationTimingType; + status?: number; }; type AnalyticsItem = { @@ -136,6 +137,15 @@ function getNav(): PerformanceNavigationTiming | null { return entry instanceof PerformanceNavigationTiming ? entry : null; } +function pageStatus(): number | undefined { + const content = document.querySelector( + 'meta[name="cheers-status"]', + )?.content; + if (content === undefined) return undefined; + const status = Number(content); + return Number.isInteger(status) ? status : undefined; +} + function trackPageView(): void { if (pageViewSent) { return; @@ -143,6 +153,7 @@ function trackPageView(): void { pageViewSent = true; const nav = getNav(); + const status = pageStatus(); enqueue({ kind: "page_view", @@ -150,6 +161,7 @@ function trackPageView(): void { context: context(), referrer: document.referrer, ...(nav?.type ? { navigation_type: nav.type } : {}), + ...(status !== undefined ? { status } : {}), }); } diff --git a/crates/cheers/src/track.rs b/crates/cheers/src/track.rs index 1cbc149..f97d687 100644 --- a/crates/cheers/src/track.rs +++ b/crates/cheers/src/track.rs @@ -1,6 +1,7 @@ use axum::{ Json, extract::{FromRequest, Request}, + http::StatusCode, response::{IntoResponse, Response}, }; use serde::{Deserialize, Serialize, de::DeserializeOwned}; @@ -170,6 +171,9 @@ pub struct PageView { pub referrer: Option, #[serde(default)] pub navigation_type: Option, + /// The HTTP status the page declared with [`PageStatus`]. + #[serde(default)] + pub status: Option, } /// An analytics event emitted manually from the page. @@ -190,6 +194,37 @@ pub struct ExceptionEvent { pub context: Context, } +/// Declares the HTTP status the page was served with, which the page view then carries. +/// +/// Safari does not expose `PerformanceNavigationTiming.responseStatus`, so a page that wants its +/// status in [`PageView::status`] renders this into its ``. +/// +/// # Example +/// +/// ``` +/// use axum::http::StatusCode; +/// use cheers::{prelude::*, track::PageStatus}; +/// +/// let rendered = html! { +/// head { (PageStatus(StatusCode::NOT_FOUND)) } +/// } +/// .render() +/// .into_inner(); +/// +/// assert!(rendered.contains(r#""#)); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PageStatus(pub StatusCode); + +impl Render for PageStatus { + fn render_to(&self, buffer: &mut Buffer) { + html! { + meta name="cheers-status" content=(self.0.as_u16()); + } + .render_to(buffer); + } +} + pub struct TrackAction(pub P); impl Render for TrackAction

{ @@ -257,6 +292,7 @@ mod tests { }, referrer: None, navigation_type: Some(NavigationType::Prerender), + status: Some(404), })] ); axum::http::StatusCode::ACCEPTED @@ -268,7 +304,7 @@ mod tests { .uri("/_track") .header("content-type", "application/json") .body(Body::from( - r#"{"service":"svc","release":"1.0.0","sent_at_ms":1,"items":[{"kind":"page_view","timestamp_ms":2,"context":{"view_id":"p1","pathname":"/a"},"referrer":null,"navigation_type":"prerender"}]}"#, + r#"{"service":"svc","release":"1.0.0","sent_at_ms":1,"items":[{"kind":"page_view","timestamp_ms":2,"context":{"view_id":"p1","pathname":"/a"},"referrer":null,"navigation_type":"prerender","status":404}]}"#, )) .expect("request should build");