@taiga-ai/monitor-web 0.1.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ @taiga-ai/monitor-web bundles the following third-party open-source software.
2
+
3
+ -------------------------------------------------------------------------------
4
+ web-vitals
5
+ https://github.com/GoogleChrome/web-vitals
6
+ Copyright 2020 Google LLC
7
+ Licensed under the Apache License, Version 2.0 (the "License").
8
+ A copy of the License is available at:
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+ License for the specific language governing permissions and limitations under
14
+ the License.
15
+
16
+ The web-vitals source is compiled into this package's distributed bundle
17
+ (dist/) rather than installed as a runtime dependency.
18
+ -------------------------------------------------------------------------------
@@ -0,0 +1,8 @@
1
+ import type { Emit } from './types.js';
2
+ /**
3
+ * Capture uncaught JS errors and unhandled promise rejections as `error` events.
4
+ * Uses passive `addEventListener` (never assigns `window.onerror`) so the host
5
+ * app's own handlers are preserved. Message/stack are length-capped; the page URL
6
+ * is scrubbed of query/fragment before send.
7
+ */
8
+ export declare function collectErrors(emit: Emit): void;
@@ -0,0 +1,18 @@
1
+ import type { BeaconEvent, MonitorConfig } from './types.js';
2
+ import { SDK_NAME, SDK_VERSION } from './version.js';
3
+ export type { BeaconEvent, MonitorConfig };
4
+ export { SDK_NAME, SDK_VERSION };
5
+ /**
6
+ * Start the Taiga monitoring beacon. Safe to call once; extra calls are no-ops.
7
+ * Collects Core Web Vitals, JS errors, and client-observed API calls, batches
8
+ * them, and sends them to the ingest keyed by the write-only `key`. Everything is
9
+ * fire-and-forget — the beacon never throws into the host app.
10
+ *
11
+ * ```ts
12
+ * import { init } from '@taiga-ai/monitor-web';
13
+ * init({ key: import.meta.env.VITE_TAIGA_MONITOR_KEY, url: import.meta.env.VITE_TAIGA_MONITOR_URL });
14
+ * ```
15
+ */
16
+ export declare function init(config: MonitorConfig): void;
17
+ /** Test-only reset of the one-shot init guard. */
18
+ export declare function __resetForTests(): void;
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ const e=`@taiga-ai/monitor-web`,t=`0.1.0`;function n(){let e=globalThis.crypto;return e&&typeof e.randomUUID==`function`?e.randomUUID():`${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`}function r(e,t){return`${e}${e.includes(`?`)?`&`:`?`}k=${encodeURIComponent(t)}`}var i=class{constructor(e){this.config=e,this.queue=[],this.fetchImpl=globalThis.fetch?globalThis.fetch.bind(globalThis):void 0}enqueue(e){let t=e;if(this.config.beforeSend)try{t=this.config.beforeSend(e)}catch{return}t&&(this.queue.push(t),this.queue.length>=50?this.flush():this.scheduleFlush())}flush(){for(this.timer!==void 0&&(clearTimeout(this.timer),this.timer=void 0);this.queue.length>0;){let e=this.queue.splice(0,50);this.send(e)}}scheduleFlush(){this.timer===void 0&&(this.timer=setTimeout(()=>this.flush(),5e3))}send(i){let a;try{a=JSON.stringify({batchId:n(),meta:{sdk:e,sdkVersion:t,sentAt:new Date().toISOString()},events:i})}catch{return}let o=r(this.config.url,this.config.key);try{let e=globalThis.navigator;if(e&&typeof e.sendBeacon==`function`&&e.sendBeacon(o,new Blob([a],{type:`text/plain`})))return}catch{}try{this.fetchImpl?.(o,{method:`POST`,body:a,keepalive:!0,headers:{"content-type":`text/plain`},mode:`cors`})}catch{}}};let a=-1;const o=e=>{addEventListener(`pageshow`,t=>{t.persisted&&(a=t.timeStamp,e(t))},!0)},s=(e,t,n,r)=>{let i,a;return o=>{t.value>=0&&(o||r)&&(a=t.value-(i??0),(a||i===void 0)&&(i=t.value,t.delta=a,t.rating=((e,t)=>e>t[1]?`poor`:e>t[0]?`needs-improvement`:`good`)(t.value,n),e(t)))}},c=e=>{requestAnimationFrame(()=>requestAnimationFrame(e))},l=()=>{let e=performance.getEntriesByType(`navigation`)[0];if(e&&e.responseStart>0&&e.responseStart<performance.now())return e},u=()=>l()?.activationStart??0,d=(e,t=-1)=>{let n=l(),r=`navigate`;return a>=0?r=`back-forward-cache`:n&&(document.prerendering||u()>0?r=`prerender`:document.wasDiscarded?r=`restore`:n.type&&(r=n.type.replace(/_/g,`-`))),{name:e,value:t,rating:`good`,delta:0,entries:[],id:`v5-${Date.now()}-${Math.floor(8999999999999*Math.random())+0xe8d4a51000}`,navigationType:r}},f=new WeakMap;function p(e,t){return f.get(e)||f.set(e,new t),f.get(e)}var m=class{t;i=0;o=[];h(e){if(e.hadRecentInput)return;let t=this.o[0],n=this.o.at(-1);this.i&&t&&n&&e.startTime-n.startTime<1e3&&e.startTime-t.startTime<5e3?(this.i+=e.value,this.o.push(e)):(this.i=e.value,this.o=[e]),this.t?.(e)}};const h=(e,t,n={})=>{try{if(PerformanceObserver.supportedEntryTypes.includes(e)){let r=new PerformanceObserver(e=>{queueMicrotask(()=>{t(e.getEntries())})});return r.observe({type:e,buffered:!0,...n}),r}}catch{}},g=e=>{let t=!1;return()=>{t||=(e(),!0)}};let _=-1;const v=new Set,y=()=>document.visibilityState!==`hidden`||document.prerendering?1/0:0,b=e=>{if(document.visibilityState===`hidden`){if(e.type===`visibilitychange`)for(let e of v)e();isFinite(_)||(_=e.type===`visibilitychange`?e.timeStamp:0,removeEventListener(`prerenderingchange`,b,!0))}},x=()=>{if(_<0){let e=u();_=(document.prerendering?void 0:globalThis.performance.getEntriesByType(`visibility-state`).find(t=>t.name===`hidden`&&t.startTime>=e)?.startTime)??y(),addEventListener(`visibilitychange`,b,!0),addEventListener(`prerenderingchange`,b,!0),o(()=>{setTimeout(()=>{_=y()})})}return{get firstHiddenTime(){return _},onHidden(e){v.add(e)}}},S=e=>{document.prerendering?addEventListener(`prerenderingchange`,e,!0):e()},C=[1800,3e3],w=(e,t={})=>{S(()=>{let n=x(),r,i=d(`FCP`),a=h(`paint`,e=>{for(let t of e)t.name===`first-contentful-paint`&&(a.disconnect(),t.startTime<n.firstHiddenTime&&(i.value=Math.max(t.startTime-u(),0),i.entries.push(t),r(!0)))});a&&(r=s(e,i,C,t.reportAllChanges),o(n=>{i=d(`FCP`),r=s(e,i,C,t.reportAllChanges),c(()=>{i.value=performance.now()-n.timeStamp,r(!0)})}))})},T=[.1,.25],E=(e,t={})=>{let n=x();w(g(()=>{let r,i=d(`CLS`,0),a=p(t,m),l=e=>{for(let t of e)a.h(t);a.i>i.value&&(i.value=a.i,i.entries=a.o,r())},u=h(`layout-shift`,l);u&&(r=s(e,i,T,t.reportAllChanges),n.onHidden(()=>{l(u.takeRecords()),r(!0)}),o(()=>{a.i=0,i=d(`CLS`,0),r=s(e,i,T,t.reportAllChanges),c(r)}),setTimeout(r))}))};let D=0,O=1/0,k=0;const ee=e=>{for(let t of e)t.interactionId&&(O=Math.min(O,t.interactionId),k=Math.max(k,t.interactionId),D=k?(k-O)/7+1:0)};let A;const j=()=>A?D:performance.interactionCount??0,M=()=>{`interactionCount`in performance||A||(A=h(`event`,ee,{durationThreshold:0}))};let N=0;var P=class{l=[];u=new Map;m;p;v(){N=j(),this.l.length=0,this.u.clear()}T(){let e=Math.min(this.l.length-1,Math.floor((j()-N)/50));return this.l[e]}h(e){if(this.m?.(e),!e.interactionId&&e.entryType!==`first-input`)return;let t=this.l.at(-1),n=this.u.get(e.interactionId);if(n||this.l.length<10||e.duration>t.L){if(n?e.duration>n.L?(n.entries=[e],n.L=e.duration):e.duration===n.L&&e.startTime===n.entries[0].startTime&&n.entries.push(e):(n={id:e.interactionId,entries:[e],L:e.duration},this.u.set(n.id,n),this.l.push(n)),this.l.sort((e,t)=>t.L-e.L),this.l.length>10){let e=this.l.splice(10);for(let t of e)this.u.delete(t.id)}this.p?.(n)}}};const F=e=>{let t=globalThis.requestIdleCallback||setTimeout,n=globalThis.cancelIdleCallback||clearTimeout;if(document.visibilityState===`hidden`)e();else{let r=g(e),i=-1,a=()=>{n(i),r()};addEventListener(`visibilitychange`,a,{once:!0,capture:!0}),i=t(()=>{removeEventListener(`visibilitychange`,a,{capture:!0}),r()})}},I=[200,500],L=(e,t={})=>{if(!globalThis.PerformanceEventTiming||!(`interactionId`in PerformanceEventTiming.prototype))return;let n=x();S(()=>{M();let r,i=d(`INP`),a=p(t,P),c=e=>{F(()=>{for(let t of e)a.h(t);let t=a.T();t&&t.L!==i.value&&(i.value=t.L,i.entries=t.entries,r())})},l=h(`event`,c,{durationThreshold:t.durationThreshold??40});r=s(e,i,I,t.reportAllChanges),l&&(l.observe({type:`first-input`,buffered:!0}),n.onHidden(()=>{c(l.takeRecords()),r(!0)}),o(()=>{a.v(),i=d(`INP`),r=s(e,i,I,t.reportAllChanges)}))})};var R=class{m;h(e){this.m?.(e)}};const z=[2500,4e3],B=(e,t={})=>{S(()=>{let n=x(),r,i=d(`LCP`),a=p(t,R),l=e=>{t.reportAllChanges||(e=e.slice(-1));for(let t of e)a.h(t),t.startTime<n.firstHiddenTime&&(i.value=Math.max(t.startTime-u(),0),i.entries=[t],r())},f=h(`largest-contentful-paint`,l);if(f){r=s(e,i,z,t.reportAllChanges);let n=g(()=>{l(f.takeRecords()),f.disconnect(),r(!0)}),a=e=>{e.isTrusted&&(F(n),removeEventListener(e.type,a,{capture:!0}))};for(let e of[`keydown`,`click`,`visibilitychange`])addEventListener(e,a,{capture:!0});o(n=>{i=d(`LCP`),r=s(e,i,z,t.reportAllChanges),c(()=>{i.value=performance.now()-n.timeStamp,r(!0)})})}})},V=[800,1800],H=e=>{document.prerendering?S(()=>H(e)):document.readyState===`complete`?setTimeout(e):addEventListener(`load`,()=>H(e),!0)},U=(e,t={})=>{let n=d(`TTFB`),r=s(e,n,V,t.reportAllChanges);H(()=>{let i=l();i&&(n.value=Math.max(i.responseStart-u(),0),n.entries=[i],r(!0),o(()=>{n=d(`TTFB`,0),r=s(e,n,V,t.reportAllChanges),r(!0)}))})},W={LCP:`ms`,INP:`ms`,FCP:`ms`,TTFB:`ms`,CLS:`score`};function G(e){let t=t=>{e({kind:`web_vital`,name:t.name,value:t.value,unit:W[t.name]??`ms`,attributes:{rating:t.rating,id:t.id,navigationType:t.navigationType},occurredAt:new Date().toISOString()})};B(t),L(t),E(t),w(t),U(t)}function K(e,t=2048){let n=globalThis.location?.href;try{let r=new URL(e,n);return`${r.origin}${r.pathname}`.slice(0,t)}catch{return(e??``).split(`?`)[0].split(`#`)[0].slice(0,t)}}function q(e,t){return String(e??``).slice(0,t)}function J(e){globalThis.addEventListener(`error`,t=>{let n=t.error;e({kind:`error`,name:n?.name??`Error`,attributes:{message:q(t.message,1e3),stack:n?.stack?q(n.stack,4e3):void 0,url:K(globalThis.location?.href??``)},occurredAt:new Date().toISOString()})}),globalThis.addEventListener(`unhandledrejection`,t=>{let n=t.reason;e({kind:`error`,name:`unhandledrejection`,attributes:{message:q(n?.message??n,1e3),stack:n?.stack?q(n.stack,4e3):void 0,url:K(globalThis.location?.href??``)},occurredAt:new Date().toISOString()})})}const Y=Symbol.for(`taiga.monitor.fetch`),X=Symbol.for(`taiga.monitor.xhr`);function Z(){return globalThis.performance?.now?.()??Date.now()}function Q(e,t,n,r){let i=K(t);return{kind:`api_call`,name:`${e} ${i}`.slice(0,120),value:Math.round(r),unit:`ms`,attributes:{method:e,url:i,statusCode:n},occurredAt:new Date().toISOString()}}function te(e,t){return(t?.method??(typeof e==`object`&&`method`in e?e.method:void 0)??`GET`).toUpperCase()}function ne(e){return typeof e==`string`?e:e instanceof URL?e.href:e.url}function re(e){let t=globalThis,n=t.fetch;typeof n!=`function`||t[Y]||(t[Y]=!0,t.fetch=function(...t){let r=Z(),i=te(t[0],t[1]),a=ne(t[0]);return Reflect.apply(n,this,t).then(t=>{try{e(Q(i,a,t.status,Z()-r))}catch{}return t},t=>{try{e(Q(i,a,0,Z()-r))}catch{}throw t})})}function ie(e){let t=globalThis.XMLHttpRequest,n=t?.prototype;if(!t||!n||n[X])return;let r=n.open,i=n.send,a=Symbol.for(`taiga.monitor.xhrctx`);n.open=function(e,t,...n){return this[a]={method:String(e).toUpperCase(),url:String(t),start:0},r.apply(this,[e,t,...n])},n.send=function(...t){let n=this[a];return n&&(n.start=Z(),this.addEventListener(`loadend`,()=>{try{e(Q(n.method,n.url,this.status,Z()-n.start))}catch{}},{once:!0})),i.apply(this,t)},n[X]=!0}let $=!1;function ae(e){if($||!e?.key||!e?.url||globalThis.window===void 0)return;$=!0;let t=e.sampleRate??1;if(!(t<1&&Math.random()>=t))try{let t=new i(e),n=e=>t.enqueue(e);e.captureVitals!==!1&&G(n),e.captureErrors!==!1&&J(n),e.captureRequests!==!1&&(re(n),ie(n));let r=()=>t.flush();globalThis.addEventListener(`visibilitychange`,()=>{globalThis.document?.visibilityState===`hidden`&&r()}),globalThis.addEventListener(`pagehide`,r)}catch{$=!1}}function oe(){$=!1}export{e as SDK_NAME,t as SDK_VERSION,oe as __resetForTests,ae as init};
@@ -0,0 +1 @@
1
+ var TaigaMonitor=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});let t=`@taiga-ai/monitor-web`,n=`0.1.0`;function r(){let e=globalThis.crypto;return e&&typeof e.randomUUID==`function`?e.randomUUID():`${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`}function i(e,t){return`${e}${e.includes(`?`)?`&`:`?`}k=${encodeURIComponent(t)}`}var a=class{constructor(e){this.config=e,this.queue=[],this.fetchImpl=globalThis.fetch?globalThis.fetch.bind(globalThis):void 0}enqueue(e){let t=e;if(this.config.beforeSend)try{t=this.config.beforeSend(e)}catch{return}t&&(this.queue.push(t),this.queue.length>=50?this.flush():this.scheduleFlush())}flush(){for(this.timer!==void 0&&(clearTimeout(this.timer),this.timer=void 0);this.queue.length>0;){let e=this.queue.splice(0,50);this.send(e)}}scheduleFlush(){this.timer===void 0&&(this.timer=setTimeout(()=>this.flush(),5e3))}send(e){let a;try{a=JSON.stringify({batchId:r(),meta:{sdk:t,sdkVersion:n,sentAt:new Date().toISOString()},events:e})}catch{return}let o=i(this.config.url,this.config.key);try{let e=globalThis.navigator;if(e&&typeof e.sendBeacon==`function`&&e.sendBeacon(o,new Blob([a],{type:`text/plain`})))return}catch{}try{this.fetchImpl?.(o,{method:`POST`,body:a,keepalive:!0,headers:{"content-type":`text/plain`},mode:`cors`})}catch{}}};let o=-1,s=e=>{addEventListener(`pageshow`,t=>{t.persisted&&(o=t.timeStamp,e(t))},!0)},c=(e,t,n,r)=>{let i,a;return o=>{t.value>=0&&(o||r)&&(a=t.value-(i??0),(a||i===void 0)&&(i=t.value,t.delta=a,t.rating=((e,t)=>e>t[1]?`poor`:e>t[0]?`needs-improvement`:`good`)(t.value,n),e(t)))}},l=e=>{requestAnimationFrame(()=>requestAnimationFrame(e))},u=()=>{let e=performance.getEntriesByType(`navigation`)[0];if(e&&e.responseStart>0&&e.responseStart<performance.now())return e},d=()=>u()?.activationStart??0,f=(e,t=-1)=>{let n=u(),r=`navigate`;return o>=0?r=`back-forward-cache`:n&&(document.prerendering||d()>0?r=`prerender`:document.wasDiscarded?r=`restore`:n.type&&(r=n.type.replace(/_/g,`-`))),{name:e,value:t,rating:`good`,delta:0,entries:[],id:`v5-${Date.now()}-${Math.floor(8999999999999*Math.random())+0xe8d4a51000}`,navigationType:r}},p=new WeakMap;function m(e,t){return p.get(e)||p.set(e,new t),p.get(e)}var h=class{t;i=0;o=[];h(e){if(e.hadRecentInput)return;let t=this.o[0],n=this.o.at(-1);this.i&&t&&n&&e.startTime-n.startTime<1e3&&e.startTime-t.startTime<5e3?(this.i+=e.value,this.o.push(e)):(this.i=e.value,this.o=[e]),this.t?.(e)}};let g=(e,t,n={})=>{try{if(PerformanceObserver.supportedEntryTypes.includes(e)){let r=new PerformanceObserver(e=>{queueMicrotask(()=>{t(e.getEntries())})});return r.observe({type:e,buffered:!0,...n}),r}}catch{}},_=e=>{let t=!1;return()=>{t||=(e(),!0)}},v=-1,y=new Set,b=()=>document.visibilityState!==`hidden`||document.prerendering?1/0:0,x=e=>{if(document.visibilityState===`hidden`){if(e.type===`visibilitychange`)for(let e of y)e();isFinite(v)||(v=e.type===`visibilitychange`?e.timeStamp:0,removeEventListener(`prerenderingchange`,x,!0))}},S=()=>{if(v<0){let e=d();v=(document.prerendering?void 0:globalThis.performance.getEntriesByType(`visibility-state`).find(t=>t.name===`hidden`&&t.startTime>=e)?.startTime)??b(),addEventListener(`visibilitychange`,x,!0),addEventListener(`prerenderingchange`,x,!0),s(()=>{setTimeout(()=>{v=b()})})}return{get firstHiddenTime(){return v},onHidden(e){y.add(e)}}},C=e=>{document.prerendering?addEventListener(`prerenderingchange`,e,!0):e()},w=[1800,3e3],T=(e,t={})=>{C(()=>{let n=S(),r,i=f(`FCP`),a=g(`paint`,e=>{for(let t of e)t.name===`first-contentful-paint`&&(a.disconnect(),t.startTime<n.firstHiddenTime&&(i.value=Math.max(t.startTime-d(),0),i.entries.push(t),r(!0)))});a&&(r=c(e,i,w,t.reportAllChanges),s(n=>{i=f(`FCP`),r=c(e,i,w,t.reportAllChanges),l(()=>{i.value=performance.now()-n.timeStamp,r(!0)})}))})},E=[.1,.25],ee=(e,t={})=>{let n=S();T(_(()=>{let r,i=f(`CLS`,0),a=m(t,h),o=e=>{for(let t of e)a.h(t);a.i>i.value&&(i.value=a.i,i.entries=a.o,r())},u=g(`layout-shift`,o);u&&(r=c(e,i,E,t.reportAllChanges),n.onHidden(()=>{o(u.takeRecords()),r(!0)}),s(()=>{a.i=0,i=f(`CLS`,0),r=c(e,i,E,t.reportAllChanges),l(r)}),setTimeout(r))}))},D=0,O=1/0,k=0,A=e=>{for(let t of e)t.interactionId&&(O=Math.min(O,t.interactionId),k=Math.max(k,t.interactionId),D=k?(k-O)/7+1:0)},j,M=()=>j?D:performance.interactionCount??0,N=()=>{`interactionCount`in performance||j||(j=g(`event`,A,{durationThreshold:0}))},P=0;var F=class{l=[];u=new Map;m;p;v(){P=M(),this.l.length=0,this.u.clear()}T(){let e=Math.min(this.l.length-1,Math.floor((M()-P)/50));return this.l[e]}h(e){if(this.m?.(e),!e.interactionId&&e.entryType!==`first-input`)return;let t=this.l.at(-1),n=this.u.get(e.interactionId);if(n||this.l.length<10||e.duration>t.L){if(n?e.duration>n.L?(n.entries=[e],n.L=e.duration):e.duration===n.L&&e.startTime===n.entries[0].startTime&&n.entries.push(e):(n={id:e.interactionId,entries:[e],L:e.duration},this.u.set(n.id,n),this.l.push(n)),this.l.sort((e,t)=>t.L-e.L),this.l.length>10){let e=this.l.splice(10);for(let t of e)this.u.delete(t.id)}this.p?.(n)}}};let I=e=>{let t=globalThis.requestIdleCallback||setTimeout,n=globalThis.cancelIdleCallback||clearTimeout;if(document.visibilityState===`hidden`)e();else{let r=_(e),i=-1,a=()=>{n(i),r()};addEventListener(`visibilitychange`,a,{once:!0,capture:!0}),i=t(()=>{removeEventListener(`visibilitychange`,a,{capture:!0}),r()})}},L=[200,500],R=(e,t={})=>{if(!globalThis.PerformanceEventTiming||!(`interactionId`in PerformanceEventTiming.prototype))return;let n=S();C(()=>{N();let r,i=f(`INP`),a=m(t,F),o=e=>{I(()=>{for(let t of e)a.h(t);let t=a.T();t&&t.L!==i.value&&(i.value=t.L,i.entries=t.entries,r())})},l=g(`event`,o,{durationThreshold:t.durationThreshold??40});r=c(e,i,L,t.reportAllChanges),l&&(l.observe({type:`first-input`,buffered:!0}),n.onHidden(()=>{o(l.takeRecords()),r(!0)}),s(()=>{a.v(),i=f(`INP`),r=c(e,i,L,t.reportAllChanges)}))})};var z=class{m;h(e){this.m?.(e)}};let B=[2500,4e3],V=(e,t={})=>{C(()=>{let n=S(),r,i=f(`LCP`),a=m(t,z),o=e=>{t.reportAllChanges||(e=e.slice(-1));for(let t of e)a.h(t),t.startTime<n.firstHiddenTime&&(i.value=Math.max(t.startTime-d(),0),i.entries=[t],r())},u=g(`largest-contentful-paint`,o);if(u){r=c(e,i,B,t.reportAllChanges);let n=_(()=>{o(u.takeRecords()),u.disconnect(),r(!0)}),a=e=>{e.isTrusted&&(I(n),removeEventListener(e.type,a,{capture:!0}))};for(let e of[`keydown`,`click`,`visibilitychange`])addEventListener(e,a,{capture:!0});s(n=>{i=f(`LCP`),r=c(e,i,B,t.reportAllChanges),l(()=>{i.value=performance.now()-n.timeStamp,r(!0)})})}})},H=[800,1800],U=e=>{document.prerendering?C(()=>U(e)):document.readyState===`complete`?setTimeout(e):addEventListener(`load`,()=>U(e),!0)},W=(e,t={})=>{let n=f(`TTFB`),r=c(e,n,H,t.reportAllChanges);U(()=>{let i=u();i&&(n.value=Math.max(i.responseStart-d(),0),n.entries=[i],r(!0),s(()=>{n=f(`TTFB`,0),r=c(e,n,H,t.reportAllChanges),r(!0)}))})},G={LCP:`ms`,INP:`ms`,FCP:`ms`,TTFB:`ms`,CLS:`score`};function K(e){let t=t=>{e({kind:`web_vital`,name:t.name,value:t.value,unit:G[t.name]??`ms`,attributes:{rating:t.rating,id:t.id,navigationType:t.navigationType},occurredAt:new Date().toISOString()})};V(t),R(t),ee(t),T(t),W(t)}function q(e,t=2048){let n=globalThis.location?.href;try{let r=new URL(e,n);return`${r.origin}${r.pathname}`.slice(0,t)}catch{return(e??``).split(`?`)[0].split(`#`)[0].slice(0,t)}}function J(e,t){return String(e??``).slice(0,t)}function te(e){globalThis.addEventListener(`error`,t=>{let n=t.error;e({kind:`error`,name:n?.name??`Error`,attributes:{message:J(t.message,1e3),stack:n?.stack?J(n.stack,4e3):void 0,url:q(globalThis.location?.href??``)},occurredAt:new Date().toISOString()})}),globalThis.addEventListener(`unhandledrejection`,t=>{let n=t.reason;e({kind:`error`,name:`unhandledrejection`,attributes:{message:J(n?.message??n,1e3),stack:n?.stack?J(n.stack,4e3):void 0,url:q(globalThis.location?.href??``)},occurredAt:new Date().toISOString()})})}let Y=Symbol.for(`taiga.monitor.fetch`),X=Symbol.for(`taiga.monitor.xhr`);function Z(){return globalThis.performance?.now?.()??Date.now()}function Q(e,t,n,r){let i=q(t);return{kind:`api_call`,name:`${e} ${i}`.slice(0,120),value:Math.round(r),unit:`ms`,attributes:{method:e,url:i,statusCode:n},occurredAt:new Date().toISOString()}}function ne(e,t){return(t?.method??(typeof e==`object`&&`method`in e?e.method:void 0)??`GET`).toUpperCase()}function re(e){return typeof e==`string`?e:e instanceof URL?e.href:e.url}function ie(e){let t=globalThis,n=t.fetch;typeof n!=`function`||t[Y]||(t[Y]=!0,t.fetch=function(...t){let r=Z(),i=ne(t[0],t[1]),a=re(t[0]);return Reflect.apply(n,this,t).then(t=>{try{e(Q(i,a,t.status,Z()-r))}catch{}return t},t=>{try{e(Q(i,a,0,Z()-r))}catch{}throw t})})}function ae(e){let t=globalThis.XMLHttpRequest,n=t?.prototype;if(!t||!n||n[X])return;let r=n.open,i=n.send,a=Symbol.for(`taiga.monitor.xhrctx`);n.open=function(e,t,...n){return this[a]={method:String(e).toUpperCase(),url:String(t),start:0},r.apply(this,[e,t,...n])},n.send=function(...t){let n=this[a];return n&&(n.start=Z(),this.addEventListener(`loadend`,()=>{try{e(Q(n.method,n.url,this.status,Z()-n.start))}catch{}},{once:!0})),i.apply(this,t)},n[X]=!0}let $=!1;function oe(e){if($||!e?.key||!e?.url||globalThis.window===void 0)return;$=!0;let t=e.sampleRate??1;if(!(t<1&&Math.random()>=t))try{let t=new a(e),n=e=>t.enqueue(e);e.captureVitals!==!1&&K(n),e.captureErrors!==!1&&te(n),e.captureRequests!==!1&&(ie(n),ae(n));let r=()=>t.flush();globalThis.addEventListener(`visibilitychange`,()=>{globalThis.document?.visibilityState===`hidden`&&r()}),globalThis.addEventListener(`pagehide`,r)}catch{$=!1}}function se(){$=!1}return e.SDK_NAME=t,e.SDK_VERSION=n,e.__resetForTests=se,e.init=oe,e})({});
@@ -0,0 +1,14 @@
1
+ import type { Emit } from './types.js';
2
+ /**
3
+ * Observe-and-annotate `fetch`: call the original via `Reflect.apply`, return its
4
+ * exact promise, record status+latency on settle, and RE-THROW on rejection —
5
+ * never swallow an error or alter the response (we don't read the body, so no
6
+ * clone is needed). Mirrors Sentry / OpenTelemetry-JS.
7
+ */
8
+ export declare function instrumentFetch(emit: Emit): void;
9
+ /**
10
+ * Observe-and-annotate `XMLHttpRequest`: wrap `open`/`send` and attach a passive
11
+ * `loadend` listener (fires on success, error, and abort — status 0 for the
12
+ * latter two). Original behavior is untouched.
13
+ */
14
+ export declare function instrumentXhr(emit: Emit): void;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Reduce a URL to `origin + pathname`, dropping the query string and fragment
3
+ * (which routinely carry PII / tokens / ids) and capping length. Client-side PII
4
+ * scrubbing before anything leaves the page. Falls back to a best-effort strip
5
+ * for values that don't parse as a URL.
6
+ */
7
+ export declare function scrubUrl(raw: string, max?: number): string;
@@ -0,0 +1,19 @@
1
+ import type { BeaconEvent, MonitorConfig } from './types.js';
2
+ /**
3
+ * Batches events and flushes them to the ingest, fire-and-forget. Uses
4
+ * `navigator.sendBeacon` (a text/plain POST that survives page discard and skips
5
+ * the CORS preflight) with a `fetch(keepalive)` fallback. Flushes when a batch
6
+ * fills, on an interval, and (driven by init) when the page is backgrounded.
7
+ * Nothing here ever throws into the host application.
8
+ */
9
+ export declare class Transport {
10
+ private readonly config;
11
+ private queue;
12
+ private timer;
13
+ private readonly fetchImpl;
14
+ constructor(config: MonitorConfig);
15
+ enqueue(event: BeaconEvent): void;
16
+ flush(): void;
17
+ private scheduleFlush;
18
+ private send;
19
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The open beacon event shape (mirrors Taiga's /v1/beacon contract): a
3
+ * self-describing measurement. `kind`/`name` are open strings; `attributes` is
4
+ * an open bag of dimensions. See @taiga/types `BeaconEventInput`.
5
+ */
6
+ export interface BeaconEvent {
7
+ kind: string;
8
+ name?: string;
9
+ value?: number;
10
+ unit?: string;
11
+ attributes?: Record<string, unknown>;
12
+ occurredAt: string;
13
+ }
14
+ /** Configuration for {@link init}. */
15
+ export interface MonitorConfig {
16
+ /** Per-environment write-only ingest key (safe to embed in the client bundle). */
17
+ key: string;
18
+ /** Ingest endpoint URL (e.g. `https://api.eu.tai.ga/beacon`). */
19
+ url: string;
20
+ /** Fraction of sessions to instrument, 0..1 (default 1). Decided once per page load. */
21
+ sampleRate?: number;
22
+ /** Collect Core Web Vitals (default true). */
23
+ captureVitals?: boolean;
24
+ /** Collect JS errors + unhandled rejections (default true). */
25
+ captureErrors?: boolean;
26
+ /** Collect client-observed API calls via fetch/XHR (default true). */
27
+ captureRequests?: boolean;
28
+ /** Transform or drop each event before it is queued (return null to drop). PII scrubbing hook. */
29
+ beforeSend?: (event: BeaconEvent) => BeaconEvent | null;
30
+ }
31
+ /** Emit an event into the transport queue. */
32
+ export type Emit = (event: BeaconEvent) => void;
@@ -0,0 +1,3 @@
1
+ /** Beacon identity, sent as `meta.sdk`/`meta.sdkVersion` for server-side skew observability. */
2
+ export declare const SDK_NAME = "@taiga-ai/monitor-web";
3
+ export declare const SDK_VERSION = "0.1.0";
@@ -0,0 +1,9 @@
1
+ import type { Emit } from './types.js';
2
+ /**
3
+ * Collect Core Web Vitals via Google's `web-vitals` (bundled). We reuse it rather
4
+ * than reimplement because it correctly handles the hard lifecycle cases (bfcache
5
+ * restores re-report with a new id, `visibilitychange` multi-fire, INP/CLS session
6
+ * windows). Each metric becomes a `web_vital` event; the rating/id/navigationType
7
+ * ride in `attributes`.
8
+ */
9
+ export declare function collectVitals(emit: Emit): void;
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@taiga-ai/monitor-web",
3
+ "version": "0.1.0-dev.1",
4
+ "description": "Taiga app monitoring — a tiny, self-contained RUM beacon (Core Web Vitals, JS errors, client-observed API health) for apps built with Taiga.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./global": "./dist/monitor-web.global.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "THIRD_PARTY_NOTICES.txt"
21
+ ],
22
+ "keywords": [
23
+ "rum",
24
+ "web-vitals",
25
+ "monitoring",
26
+ "telemetry",
27
+ "taiga"
28
+ ],
29
+ "license": "MIT",
30
+ "scripts": {
31
+ "build": "tsc --noEmit && node scripts/build.mjs",
32
+ "typecheck": "tsc --noEmit",
33
+ "lint": "oxlint src",
34
+ "format": "oxfmt --check \"**/*.{ts,tsx,js,jsx,mjs,cjs,json,md}\"",
35
+ "format:fix": "oxfmt \"**/*.{ts,tsx,js,jsx,mjs,cjs,json,md}\"",
36
+ "test": "vitest run"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.19.17",
40
+ "jsdom": "^27.4.0",
41
+ "rolldown": "^1.0.3",
42
+ "typescript": "^7.0.2",
43
+ "vitest": "^4.1.8",
44
+ "web-vitals": "^5.1.0"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }