1use log::LevelFilter;
2
3#[cfg(target_arch = "wasm32")]
4use log::{Level, Log, Metadata, Record};
5
6#[cfg(target_arch = "wasm32")]
7use wasm_bindgen::prelude::*;
8#[cfg(target_arch = "wasm32")]
9use web_sys::{console, window};
10
11#[derive(Clone, Debug)]
12pub struct MatomoConfig {
13 pub base_url: Option<String>, pub site_id: Option<u32>,
15 pub console_level: LevelFilter,
16 pub matomo_level: LevelFilter,
17 pub inject_snippet: bool,
18}
19
20impl Default for MatomoConfig {
21 fn default() -> Self {
22 Self {
23 base_url: None,
24 site_id: None,
25 console_level: LevelFilter::Info,
26 matomo_level: LevelFilter::Warn,
27 inject_snippet: true,
28 }
29 }
30}
31
32pub struct MatomoLogger {
33 #[allow(dead_code)]
34 min_console_level: LevelFilter,
35 #[allow(dead_code)]
36 min_matomo_level: LevelFilter,
37}
38
39impl MatomoLogger {
41 pub fn init(config: MatomoConfig) -> Result<(), log::SetLoggerError> {
42 imp::init(config)
43 }
44}
45
46pub fn track_page_view(title: &str, url: &str) {
47 imp::track_page_view_impl(title, url)
48}
49pub fn set_user_id(user_id: &str) {
50 imp::set_user_id_impl(user_id)
51}
52
53#[cfg(target_arch = "wasm32")]
54fn log_to_console(threshold: LevelFilter, record: &Record) {
55 if record.level().to_level_filter() > threshold {
56 return;
57 }
58 let msg = format!(
59 "{}: {} — {}",
60 record.level(),
61 record.target(),
62 record.args()
63 );
64 match record.level() {
65 Level::Error => console::error_1(&JsValue::from_str(&msg)),
66 Level::Warn => console::warn_1(&JsValue::from_str(&msg)),
67 Level::Info => console::info_1(&JsValue::from_str(&msg)),
68 Level::Debug => console::log_1(&JsValue::from_str(&msg)),
69 Level::Trace => console::debug_1(&JsValue::from_str(&msg)),
70 }
71}
72
73#[cfg(target_arch = "wasm32")]
74fn log_to_matomo(threshold: LevelFilter, record: &Record) {
75 if record.level().to_level_filter() > threshold {
76 return;
77 }
78 let mut name = format!("{} — {}", record.target(), record.args());
79 if name.len() > 300 {
80 name.truncate(300);
81 }
82
83 let arr = js_sys::Array::new();
84 arr.push(&JsValue::from_str("trackEvent"));
85 arr.push(&JsValue::from_str("RustLog"));
86 arr.push(&JsValue::from_str(record.level().as_str()));
87 arr.push(&JsValue::from_str(&name));
88 let value = match record.level() {
89 Level::Error => 50,
90 Level::Warn => 40,
91 Level::Info => 30,
92 Level::Debug => 20,
93 Level::Trace => 10,
94 } as f64;
95 arr.push(&JsValue::from_f64(value));
96
97 if has_paq() {
98 push_to_paq(&arr.into());
99 }
100}
101
102#[cfg(target_arch = "wasm32")]
103fn has_paq() -> bool {
104 if let Some(w) = window() {
105 js_sys::Reflect::has(&w, &JsValue::from_str("_paq")).unwrap_or(false)
106 } else {
107 false
108 }
109}
110
111#[cfg(target_arch = "wasm32")]
112fn push_to_paq(args: &JsValue) {
113 if let Some(w) = window() {
114 if let Ok(paq) = js_sys::Reflect::get(&w, &JsValue::from_str("_paq")) {
115 if let Ok(push) = js_sys::Reflect::get(&paq, &JsValue::from_str("push")) {
116 let func: js_sys::Function = push.into();
117 if let Err(e) = func.call1(&paq, args) {
119 console::error_2(&JsValue::from_str("_paq.push error"), &e);
120 }
121 }
122 }
123 }
124}
125
126#[cfg(target_arch = "wasm32")]
127fn maybe_inject_snippet(cfg: &MatomoConfig) {
128 let Some(w) = window() else {
130 return;
131 };
132 if has_paq() {
133 return;
134 }
135 let Some(base) = cfg.base_url.as_ref() else {
136 return;
137 };
138 let Some(site) = cfg.site_id else {
139 return;
140 };
141
142 let paq = js_sys::Array::new();
144 let _ = js_sys::Reflect::set(&w, &JsValue::from_str("_paq"), &paq);
145
146 let set_tracker = js_sys::Array::new();
147 set_tracker.push(&JsValue::from_str("setTrackerUrl"));
148 set_tracker.push(&JsValue::from_str(&(base.to_string() + "matomo.php")));
149 push_to_paq(&set_tracker.into());
150
151 let set_site = js_sys::Array::new();
152 set_site.push(&JsValue::from_str("setSiteId"));
153 set_site.push(&JsValue::from_str(&site.to_string()));
154 push_to_paq(&set_site.into());
155
156 let enable = js_sys::Array::new();
157 enable.push(&JsValue::from_str("enableLinkTracking"));
158 push_to_paq(&enable.into());
159
160 if let Some(doc) = w.document() {
162 if let Ok(script) = doc.create_element("script") {
163 script.set_attribute("async", "true").ok();
164 script
165 .set_attribute("src", &(base.to_string() + "matomo.js"))
166 .ok();
167 if let Some(head) = doc.head() {
168 let _ = head.append_child(&script);
169 }
170 }
171 }
172}
173
174#[cfg(all(target_arch = "wasm32", feature = "worker"))]
176pub mod worker {
177 use super::*;
178 use web_sys::DedicatedWorkerGlobalScope;
179
180 pub fn init_with_bridge(
183 console_level: LevelFilter,
184 matomo_level: LevelFilter,
185 _sender: js_sys::Function,
186 ) -> Result<(), log::SetLoggerError> {
187 let logger = BridgeLogger {
188 console_level,
189 matomo_level,
190 };
191 let leaked: &'static BridgeLogger = Box::leak(Box::new(logger));
193 log::set_logger(leaked)?;
194 log::set_max_level(console_level.max(matomo_level));
195 Ok(())
196 }
197
198 struct BridgeLogger {
199 console_level: LevelFilter,
200 matomo_level: LevelFilter,
201 }
202 impl Log for BridgeLogger {
203 fn enabled(&self, _metadata: &Metadata) -> bool {
204 true
205 }
206 fn log(&self, record: &Record) {
207 super::log_to_console(self.console_level, record);
208 if record.level().to_level_filter() > self.matomo_level {
209 return;
210 }
211 let obj = js_sys::Object::new();
213 let _ =
214 js_sys::Reflect::set(&obj, &JsValue::from_str("type"), &JsValue::from_str("log"));
215 let _ = js_sys::Reflect::set(
216 &obj,
217 &JsValue::from_str("level"),
218 &JsValue::from_str(record.level().as_str()),
219 );
220 let _ = js_sys::Reflect::set(
221 &obj,
222 &JsValue::from_str("target"),
223 &JsValue::from_str(record.target()),
224 );
225 let _ = js_sys::Reflect::set(
226 &obj,
227 &JsValue::from_str("message"),
228 &JsValue::from_str(&format!("{}", record.args())),
229 );
230 let scope: DedicatedWorkerGlobalScope = js_sys::global().unchecked_into();
231 let _ = scope.post_message(&obj);
232 }
233 fn flush(&self) {}
234 }
235}
236
237#[cfg(not(all(target_arch = "wasm32", feature = "worker")))]
239pub mod worker {
240 use super::*;
241 pub fn init_with_bridge<T>(
243 _console_level: LevelFilter,
244 _matomo_level: LevelFilter,
245 _sender: T,
246 ) -> Result<(), log::SetLoggerError> {
247 Ok(())
248 }
249}
250
251#[cfg(target_arch = "wasm32")]
255mod imp {
256 use super::*;
257
258 impl Log for MatomoLogger {
259 fn enabled(&self, _metadata: &Metadata) -> bool {
260 true
261 }
262 fn log(&self, record: &Record) {
263 super::log_to_console(self.min_console_level, record);
264 super::log_to_matomo(self.min_matomo_level, record);
265 }
266 fn flush(&self) {}
267 }
268
269 pub fn init(config: MatomoConfig) -> Result<(), log::SetLoggerError> {
270 if config.inject_snippet {
271 super::maybe_inject_snippet(&config);
272 }
273 let logger = MatomoLogger {
274 min_console_level: config.console_level,
275 min_matomo_level: config.matomo_level,
276 };
277 let leaked: &'static MatomoLogger = Box::leak(Box::new(logger));
278 log::set_logger(leaked)?;
279 log::set_max_level(config.console_level.max(config.matomo_level));
280 Ok(())
281 }
282
283 pub fn track_page_view_impl(title: &str, url: &str) {
284 let a = js_sys::Array::new();
287 a.push(&JsValue::from_str("setCustomUrl"));
288 a.push(&JsValue::from_str(url));
289 if super::has_paq() {
290 super::push_to_paq(&a.into());
291 }
292
293 let a = js_sys::Array::new();
295 a.push(&JsValue::from_str("setDocumentTitle"));
296 a.push(&JsValue::from_str(title));
297 if super::has_paq() {
298 super::push_to_paq(&a.into());
299 }
300
301 let a = js_sys::Array::new();
303 a.push(&JsValue::from_str("trackPageView"));
304 if super::has_paq() {
305 super::push_to_paq(&a.into());
306 }
307 }
308 pub fn set_user_id_impl(user_id: &str) {
309 let a = js_sys::Array::new();
311 a.push(&JsValue::from_str("setUserId"));
312 a.push(&JsValue::from_str(user_id));
313 if super::has_paq() {
314 super::push_to_paq(&a.into());
315 }
316 }
317}
318
319#[cfg(not(target_arch = "wasm32"))]
321mod imp {
322 use super::*;
323 pub fn init(_config: MatomoConfig) -> Result<(), log::SetLoggerError> {
324 log::set_max_level(LevelFilter::Info);
325 Ok(())
326 }
327 pub fn track_page_view_impl(_title: &str, _url: &str) {}
328 pub fn set_user_id_impl(_user_id: &str) {}
329}