Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions crates/cheers/src/router/assets/track-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type PageViewItem = {
context: TrackContext;
referrer?: string;
navigation_type?: NavigationTimingType;
status?: number;
};

type AnalyticsItem = {
Expand Down Expand Up @@ -136,20 +137,31 @@ function getNav(): PerformanceNavigationTiming | null {
return entry instanceof PerformanceNavigationTiming ? entry : null;
}

function pageStatus(): number | undefined {
const content = document.querySelector<HTMLMetaElement>(
'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;
}
pageViewSent = true;

const nav = getNav();
const status = pageStatus();

enqueue({
kind: "page_view",
timestamp_ms: Date.now(),
context: context(),
referrer: document.referrer,
...(nav?.type ? { navigation_type: nav.type } : {}),
...(status !== undefined ? { status } : {}),
});
}

Expand Down
38 changes: 37 additions & 1 deletion crates/cheers/src/track.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use axum::{
Json,
extract::{FromRequest, Request},
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
Expand Down Expand Up @@ -170,6 +171,9 @@ pub struct PageView {
pub referrer: Option<String>,
#[serde(default)]
pub navigation_type: Option<NavigationType>,
/// The HTTP status the page declared with [`PageStatus`].
#[serde(default)]
pub status: Option<u16>,
}

/// An analytics event emitted manually from the page.
Expand All @@ -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 `<head>`.
///
/// # 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#"<meta name="cheers-status" content="404">"#));
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PageStatus(pub StatusCode);

impl Render for PageStatus {
fn render_to(&self, buffer: &mut Buffer<crate::context::Element>) {
html! {
meta name="cheers-status" content=(self.0.as_u16());
}
.render_to(buffer);
}
}

pub struct TrackAction<P: Serialize>(pub P);

impl<P: Serialize> Render<DatastarSource> for TrackAction<P> {
Expand Down Expand Up @@ -257,6 +292,7 @@ mod tests {
},
referrer: None,
navigation_type: Some(NavigationType::Prerender),
status: Some(404),
})]
);
axum::http::StatusCode::ACCEPTED
Expand All @@ -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");

Expand Down
Loading