tracwell 0.4.0 → 0.5.0

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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # SDK
2
2
 
3
3
  Browser analytics SDK for page views, custom events, identity, batching, and
4
- bounded delivery retries.
4
+ delivery retries that outlast short Collector outages.
5
5
 
6
6
  ## Script installation
7
7
 
@@ -48,9 +48,8 @@ window.tracwell?.track("signup_completed", { plan: "starter" });
48
48
 
49
49
  The script derives `/v1/events` from its own origin, so the same artifact works
50
50
  against the loopback Collector during local development. The production bundle
51
- reports SDK version `0.2.0`, targets ES2022-capable browsers, and has a 4.7 KB
52
- gzip budget enforced by
53
- `build:cdn`.
51
+ reports SDK version `0.2.0`, targets ES2022-capable browsers, and has the
52
+ accepted 5 KB (5,120-byte) gzip budget enforced by `build:cdn`.
54
53
 
55
54
  ## Public API
56
55
 
@@ -103,7 +102,7 @@ batching, retries, consent, and delivery remain in this core package.
103
102
  context.
104
103
  - Expose typed custom-event and identity APIs.
105
104
  - Batch only against the limits exported by `@tracwell/contracts`.
106
- - Retry bounded transient failures without changing event IDs.
105
+ - Retry transient failures with backoff without changing event IDs.
107
106
  - Respect configured consent and Do Not Track behavior before collection.
108
107
 
109
108
  The default SDK entry must remain browser-only, small, non-blocking, and unable
@@ -163,8 +162,19 @@ through `onError` and uses in-memory identity for the page lifetime.
163
162
  the shared 48 KiB limit.
164
163
  - Normal delivery reports `accepted` only after a matching `202` receipt
165
164
  confirms the batch ID and event count.
166
- - Network errors, `408`, `429`, and `5xx` responses use bounded in-memory retry
167
- without changing event or batch IDs.
165
+ - Network errors, `408`, `429`, and `5xx` responses are retried with backoff
166
+ (2 s doubling, at most 5 minutes per wait) without changing event or batch
167
+ IDs, until the oldest event nears the Collector's 24-hour age limit. An
168
+ explicit `maxRetries` (0-5) caps attempts; by default there is no fixed cap.
169
+ Periodic flushes wait for a scheduled retry; `flush()` and `shutdown()` send
170
+ immediately.
171
+ - While delivery is failing at most 100 events are held; the oldest waiting
172
+ events are dropped and reported through `onError`.
173
+ - Product mode with `persistence: "localStorage"` also stores held events under
174
+ `tracwell:<projectKey>:pending` and resumes them on the next page load. Private
175
+ mode never writes them to browser storage. Denying consent clears them.
176
+ Resending an event a beacon already delivered is safe: event IDs are stable
177
+ and deduplicated.
168
178
  - Other `4xx` responses report `DELIVERY_REJECTED` and are not retried.
169
179
  - Page-hide and hidden-document delivery prefers `sendBeacon`; a successful
170
180
  browser handoff reports `handed_off`, never `accepted`.
@@ -2,7 +2,17 @@ import type { NormalizedConfig, SdkError, TracwellConfig } from "./types";
2
2
  export declare const DEFAULT_COLLECTOR_ENDPOINT = "https://collect.tracwell.app/v1/events";
3
3
  export declare const DEFAULT_BATCH_SIZE = 25;
4
4
  export declare const DEFAULT_FLUSH_INTERVAL_MS = 10000;
5
- export declare const DEFAULT_MAX_RETRIES = 2;
5
+ /**
6
+ * No fixed attempt cap: a failed batch is retried with backoff until its oldest
7
+ * event approaches the Collector's event-age limit. An explicit `maxRetries`
8
+ * (0-5) still caps attempts.
9
+ */
10
+ export declare const DEFAULT_MAX_RETRIES: number;
11
+ /** Retry backoff doubles from 2 s up to 5 min. The Collector sends no Retry-After. */
12
+ export declare const RETRY_BASE_DELAY_MS = 2000;
13
+ export declare const RETRY_MAX_DELAY_MS: number;
14
+ /** Events held while delivery is failing; the oldest waiting events are dropped beyond it. */
15
+ export declare const MAX_HELD_EVENTS = 100;
6
16
  export declare const DEFAULT_SESSION_TIMEOUT_MS: number;
7
17
  export declare const SDK_VERSION = "0.2.0";
8
18
  export declare function normalizeProjectKey(value: string, onError?: (error: SdkError) => void): string;
package/dist/npm/index.js CHANGED
@@ -1 +1 @@
1
- var k="https://collect.tracwell.app/v1/events",O=25,H=1e4,B=2,X=1800*1e3,b="0.2.0";function E(e,t){throw t?.({code:"CONFIG_INVALID",message:e}),new TypeError(e)}function et(e,t){let n=e.trim();return n.length<20||n.length>128||!/^[A-Za-z0-9_-]+$/u.test(n)?E("projectKey does not match the public project-key contract.",t):n}function nt(e,t){let n;try{n=new URL(e)}catch{return E("endpoint must be an absolute HTTPS URL.",t)}return rt(n,t)}function rt(e,t){let n=e.hostname==="localhost"||e.hostname==="127.0.0.1"||e.hostname==="[::1]";return e.protocol!=="https:"&&!n?E("endpoint must use HTTPS unless it uses a loopback host.",t):e.toString()}function y(e,t,n,r,i){return!Number.isSafeInteger(t)||t<n||t>r?E(`${e} must be an integer between ${n} and ${r}.`,i):t}function G(e){let t=et(e.projectKey,e.onError),n=e.collectionMode??"private",r=nt(e.endpoint??k,e.onError);return n==="private"&&e.persistence!==void 0&&e.persistence!=="none"?E('Private collection requires persistence: "none".',e.onError):{autoTrackPageViews:e.autoTrackPageViews??!0,batchSize:y("batchSize",e.batchSize??O,1,50,e.onError),consent:e.consent??"granted",collectionMode:n,endpoint:r,flushIntervalMs:y("flushIntervalMs",e.flushIntervalMs??H,1e3,6e4,e.onError),maxRetries:y("maxRetries",e.maxRetries??B,0,5,e.onError),persistence:n==="private"?"none":e.persistence??"localStorage",projectKey:t,respectDoNotTrack:e.respectDoNotTrack??!0,sessionTimeoutMs:y("sessionTimeoutMs",e.sessionTimeoutMs??X,6e4,1440*60*1e3,e.onError),...e.onDelivery?{onDelivery:e.onDelivery}:{},...e.onError?{onError:e.onError}:{}}}var Y=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"];function it(e){let t=e?.trim();return t||void 0}function ot(e){if(e)try{return new URL(e).toString()}catch{return}}function A(e){let t=new URL(e.href),n={},r=ot(e.referrer);r&&(n.referrer=r);for(let i of Y){let o=it(t.searchParams.get(i));o&&(n[i]=o)}return n}function R(e){let t=Y.map(n=>e[n]??"").join("");return t.replaceAll("","")?t:void 0}var w=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu,yt=/^[A-Za-z][A-Za-z0-9_.:-]*$/u,Tt=/[\s\x00-\x1f\x7f]/u,St=/[\x00-\x1f\x7f]/u;function C(e){return new TextEncoder().encode(JSON.stringify(e)).byteLength}function N(e){return e.length>0&&e.length<=256&&!Tt.test(e)}function K(e,t=2048){if(e.length>t)return!1;try{return new URL(e).origin.length>0}catch{return!1}}function L(e,t,n){return t>4?!1:typeof e=="string"?e.length<=1024:typeof e=="number"?Number.isFinite(e):e===null||typeof e=="boolean"?!0:Array.isArray(e)?e.length<=50&&e.every(r=>L(r,t+1,n)):Object.entries(e).every(([r,i])=>(n.count+=1,n.count<=100&&r.length>0&&r.length<=128&&!St.test(r)&&L(i,t+1,n)))}function z(e){let{context:t}=e;try{if(C(e)>16384)return!1}catch{return!1}return w.test(e.event_id)&&N(e.anonymous_id)&&N(e.session_id)&&(e.user_id===void 0||N(e.user_id))&&(e.event_type!=="event"||e.event_name.length<=100&&yt.test(e.event_name)&&e.event_name!=="identify"&&e.event_name!=="page_view")&&K(t.url)&&t.path.startsWith("/")&&t.path.length<=1024&&(t.title===void 0||t.title.length<=512)&&(t.referrer===void 0||K(t.referrer))&&[t.utm_source,t.utm_medium,t.utm_campaign,t.utm_term,t.utm_content].every(n=>n===void 0||n.length>0&&n.length<=256)&&(e.properties===void 0||L(e.properties,0,{count:0}))}function j(e){if(!e||typeof e!="object")return;let t=e;if(!(t.receipt_version!==1||typeof t.receipt_id!="string"||!w.test(t.receipt_id)||typeof t.batch_id!="string"||!w.test(t.batch_id)||!Number.isInteger(t.accepted_events)||(t.accepted_events??0)<1||(t.accepted_events??0)>50||typeof t.accepted_at!="string"||Number.isNaN(Date.parse(t.accepted_at))))return t}function It(e,t){let n=new URL(e.href);return n.hash="",{path:n.pathname,url:n.toString(),...e.title?{title:e.title}:{},...t}}function F(e,t,n,r){let i={schema_version:1,event_id:e.randomUUID(),event_name:n.eventName,event_type:n.eventType,timestamp:new Date(e.now()).toISOString(),anonymous_id:t.anonymousId,session_id:t.sessionId,context:It(e.page(),t.attribution),...n.properties?{properties:n.properties}:{},...n.userId?{user_id:n.userId}:{}};if(!z(i)){r?.({code:"EVENT_INVALID",message:"Event was not queued because it failed the v1 contract."});return}return i}var J=1;function gt(e){if(!e||typeof e!="object")return!1;let t=e;return t.version===J&&typeof t.anonymousId=="string"&&typeof t.sessionId=="string"&&typeof t.lastActivityAt=="number"&&typeof t.attribution=="object"&&t.attribution!==null&&(t.userId===void 0||typeof t.userId=="string")}var T=class{#e;#o;#t;#i;#r;#n;constructor(t,n){this.#t=t,this.#o=n.onError,this.#i=n.sessionTimeoutMs,this.#e=`tracwell:${n.projectKey}:identity`,this.#r=n.persistence==="localStorage"?this.#p(t):void 0}identify(t,n){let r=this.#d(n);return r.userId&&r.userId!==t&&(r.anonymousId=this.#t.randomUUID(),r.sessionId=this.#t.randomUUID()),r.userId=t,this.#h(r),this.#s(r)}reset(){if(this.#n=void 0,this.#r)try{this.#r.removeItem(this.#e)}catch(t){this.#a(t)}}snapshot(t){return this.#s(this.#d(t))}#c(t){let n=this.#t.now();return{anonymousId:this.#t.randomUUID(),attribution:t,lastActivityAt:n,sessionId:this.#t.randomUUID(),version:J}}#d(t){let n=this.#t.now(),r=this.#u();if(!r){let a=this.#c(t);return this.#h(a),a}let i=R(r.attribution),o=R(t);return(n-r.lastActivityAt>=this.#i||o!==void 0&&o!==i)&&(r.sessionId=this.#t.randomUUID(),r.attribution=t),r.lastActivityAt=n,this.#h(r),r}#u(){if(this.#n)return this.#n;if(this.#r)try{let t=this.#r.getItem(this.#e);if(!t)return;let n=JSON.parse(t);if(!gt(n)){this.#r.removeItem(this.#e);return}return this.#n=n,n}catch(t){this.#a(t);return}}#p(t){try{return t.storage()}catch(n){this.#a(n);return}}#a(t){this.#o?.({cause:t,code:"STORAGE_UNAVAILABLE",message:"Persistent identity storage is unavailable; using in-memory identity."})}#s(t){return{anonymousId:t.anonymousId,attribution:t.attribution,sessionId:t.sessionId,...t.userId?{userId:t.userId}:{}}}#h(t){if(this.#n=t,!!this.#r)try{this.#r.setItem(this.#e,JSON.stringify(t))}catch(n){this.#a(n)}}};function f(e,t,n,r){return{...r===void 0?{}:{cause:r},code:e,eventIds:n.batch.events.map(i=>i.event_id),message:t}}var S=class{#e;#o;#t;#i;#r;#n=[];constructor(t,n){this.#o=t,this.#e=n}enqueue(t){this.#n.push(t),this.#n.length>=this.#e.batchSize&&this.flush()}clear(){this.#i?.(),this.#i=void 0,this.#t=void 0,this.#n=[]}flush(){return this.#r?this.#r:(this.#r=this.#c().finally(()=>{this.#r=void 0}),this.#r)}flushWithBeacon(){for(this.#i?.(),this.#i=void 0;this.#t||this.#n.length>0;){let t=this.#t??this.#d();if(!t)return;let n=JSON.stringify(t.batch);if(!this.#o.sendBeacon(this.#e.endpoint,n)){this.#e.onError?.(f("DELIVERY_FAILED","Browser lifecycle handoff was rejected by sendBeacon.",t));return}this.#t=void 0,this.#u({batchId:t.batch.batch_id,eventIds:t.batch.events.map(r=>r.event_id),status:"handed_off"})}}async#c(){this.#i?.(),this.#i=void 0;let t=this.#t??this.#d();if(!t)return;await this.#a(t)&&this.#n.length>0&&await this.#c()}#d(){if(this.#n.length===0)return;let t=this.#n.slice(0,this.#e.batchSize),n;for(;t.length>0;){let r={batch_version:1,batch_id:this.#o.randomUUID(),project_key:this.#e.projectKey,sent_at:new Date(this.#o.now()).toISOString(),sdk:{name:"tracwell-browser",version:b},events:t};if(C(r)<=49152){n=r;break}t.pop()}if(!n){let r=this.#n.shift();r&&this.#e.onError?.({code:"EVENT_INVALID",eventIds:[r.event_id],message:"Event cannot fit in a valid browser batch."});return}return this.#n.splice(0,n.events.length),this.#t={attempt:0,batch:n},this.#t}#u(t){this.#e.onDelivery?.(t)}#p(t){let n=250*2**(t.attempt-1);this.#i=this.#o.setTimeout(()=>{this.#i=void 0,this.flush()},n)}async#a(t){let n;try{n=await this.#o.fetch(this.#e.endpoint,JSON.stringify(t.batch),"text/plain;charset=UTF-8")}catch(o){return this.#t!==t?!1:this.#s(t,o)}if(n.status!==202)return n.status===408||n.status===429||n.status>=500?this.#s(t,new Error(`Collector returned HTTP ${n.status}.`)):(this.#t===t&&(this.#t=void 0),this.#e.onError?.(f("DELIVERY_REJECTED",`Collector rejected the batch with non-retryable HTTP ${n.status}.`,t)),!1);let r;try{r=await n.json()}catch(o){return this.#e.onError?.(f("RECEIPT_INVALID","Collector returned an unreadable acceptance receipt.",t,o)),this.#s(t,o)}let i=j(r);return!i||i.batch_id!==t.batch.batch_id||i.accepted_events!==t.batch.events.length?(this.#e.onError?.(f("RECEIPT_INVALID","Collector receipt does not match the submitted batch.",t)),this.#s(t,new Error("Acceptance receipt mismatch."))):(this.#t===t&&(this.#t=void 0),this.#u({batchId:t.batch.batch_id,eventIds:t.batch.events.map(o=>o.event_id),receiptId:i.receipt_id,status:"accepted"}),!0)}#s(t,n){return this.#t!==t?!1:(t.attempt+=1,t.attempt<=this.#e.maxRetries?(this.#e.onError?.(f("DELIVERY_FAILED","Event batch delivery failed and is scheduled for bounded retry.",t,n)),this.#p(t),!1):(this.#t=void 0,this.#e.onError?.(f("DELIVERY_EXHAUSTED","Event batch delivery exhausted the configured retry limit.",t,n)),!1))}};function $(e,t){let n=new T(e,t),r=new S(e,t),i=t.consent,o,p=!1,c=!1,a=[];function _(){return i==="granted"&&!(t.respectDoNotTrack&&e.doNotTrackEnabled())}function I(s,u,d,h){if(c||!_())return;let V=A(e.page()),D=n.snapshot(V),l=h??D.userId,Q=s==="identify"&&l?{eventName:"identify",eventType:"identify",...d?{properties:d}:{},userId:l}:s==="pageview"?{eventName:"page_view",eventType:"pageview",...d?{properties:d}:{},...l?{userId:l}:{}}:{eventName:u,eventType:"event",...d?{properties:d}:{},...l?{userId:l}:{}},m=F(e,D,Q,t.onError);if(m){if(s==="identify"&&h){let U=n.identify(h,V);m.anonymous_id=U.anonymousId,m.session_id=U.sessionId}return r.enqueue(m),m.event_id}}function g(s){if(!_())return;let u=new URL(e.page().href);u.hash="";let d=u.toString();if(d===o)return;let h=I("pageview","page_view",s);return h&&(o=d),h}function x(){a.forEach(s=>s()),a=[],p=!1}function M(){p||c||!_()||(p=!0,a=[e.addPageHideListener(()=>r.flushWithBeacon()),e.addVisibilityHiddenListener(()=>r.flushWithBeacon()),e.setInterval(()=>{r.flush()},t.flushIntervalMs)],t.autoTrackPageViews&&(a.push(e.subscribeToNavigation(()=>{g()})),g()))}return M(),{flush(){return r.flush()},getSession(){if(c||t.collectionMode!=="product"||!_())return;let s=n.snapshot(A(e.page()));return{anonymousId:s.anonymousId,sessionId:s.sessionId,...s.userId?{userId:s.userId}:{}}},identify(s,u){if(t.collectionMode==="private"){t.onError?.({code:"IDENTIFY_UNAVAILABLE",message:"identify() is unavailable in Private collection mode."});return}return I("identify","identify",u,s)},page:g,reset(){n.reset(),o=void 0},setConsent(s){if(i=s,s==="denied"){x(),r.clear(),n.reset(),o=void 0;return}M()},async shutdown(){c||(x(),await r.flush(),c=!0)},track(s,u){return I("event",s,u)}}}function q(e,t){return window.addEventListener(e,t),()=>window.removeEventListener(e,t)}function W(){return{addPageHideListener(e){return q("pagehide",e)},addVisibilityHiddenListener(e){let t=()=>{document.visibilityState==="hidden"&&e()};return document.addEventListener("visibilitychange",t),()=>document.removeEventListener("visibilitychange",t)},doNotTrackEnabled(){return navigator.doNotTrack==="1"},async fetch(e,t,n){return fetch(e,{body:t,credentials:"omit",headers:{"content-type":n},keepalive:!0,method:"POST"})},now(){return Date.now()},page(){return{href:window.location.href,referrer:document.referrer,title:document.title}},randomUUID(){return crypto.randomUUID()},sendBeacon(e,t){return navigator.sendBeacon(e,t)},setInterval(e,t){let n=window.setInterval(e,t);return()=>window.clearInterval(n)},setTimeout(e,t){let n=window.setTimeout(e,t);return()=>window.clearTimeout(n)},storage(){return window.localStorage},subscribeToNavigation(e){let t=!0,n=history.pushState,r=history.replaceState,i=()=>{t&&queueMicrotask(e)},o=function(...a){n.apply(history,a),i()},p=function(...a){r.apply(history,a),i()};history.pushState=o,history.replaceState=p;let c=q("popstate",e);return()=>{t=!1,c(),history.pushState===o&&(history.pushState=n),history.replaceState===p&&(history.replaceState=r)}}}}function Jt(e){if(typeof window>"u"||typeof document>"u"||typeof navigator>"u")throw new Error("createTracwell must be called in a browser after the document is available.");return $(W(),G(e))}export{O as DEFAULT_BATCH_SIZE,k as DEFAULT_COLLECTOR_ENDPOINT,H as DEFAULT_FLUSH_INTERVAL_MS,B as DEFAULT_MAX_RETRIES,X as DEFAULT_SESSION_TIMEOUT_MS,b as SDK_VERSION,Jt as createTracwell};
1
+ var O="https://collect.tracwell.app/v1/events",H=25,B=1e4,X=Number.POSITIVE_INFINITY,Y=2e3,G=300*1e3,A=100,K=1800*1e3,b="0.2.0";function E(t,e){throw e?.({code:"CONFIG_INVALID",message:t}),new TypeError(t)}function oe(t,e){let n=t.trim();return n.length<20||n.length>128||!/^[A-Za-z0-9_-]+$/u.test(n)?E("projectKey does not match the public project-key contract.",e):n}function se(t,e){let n;try{n=new URL(t)}catch{return E("endpoint must be an absolute HTTPS URL.",e)}return ae(n,e)}function ae(t,e){let n=t.hostname==="localhost"||t.hostname==="127.0.0.1"||t.hostname==="[::1]";return t.protocol!=="https:"&&!n?E("endpoint must use HTTPS unless it uses a loopback host.",e):t.toString()}function y(t,e,n,r,i){return!Number.isSafeInteger(e)||e<n||e>r?E(`${t} must be an integer between ${n} and ${r}.`,i):e}function z(t){let e=oe(t.projectKey,t.onError),n=t.collectionMode??"private",r=se(t.endpoint??O,t.onError);return n==="private"&&t.persistence!==void 0&&t.persistence!=="none"?E('Private collection requires persistence: "none".',t.onError):{autoTrackPageViews:t.autoTrackPageViews??!0,batchSize:y("batchSize",t.batchSize??H,1,50,t.onError),consent:t.consent??"granted",collectionMode:n,endpoint:r,flushIntervalMs:y("flushIntervalMs",t.flushIntervalMs??B,1e3,6e4,t.onError),maxRetries:t.maxRetries===void 0?X:y("maxRetries",t.maxRetries,0,5,t.onError),persistence:n==="private"?"none":t.persistence??"localStorage",projectKey:e,respectDoNotTrack:t.respectDoNotTrack??!0,sessionTimeoutMs:y("sessionTimeoutMs",t.sessionTimeoutMs??K,6e4,1440*60*1e3,t.onError),...t.onDelivery?{onDelivery:t.onDelivery}:{},...t.onError?{onError:t.onError}:{}}}var j=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"];function de(t){let e=t?.trim();return e||void 0}function ce(t){if(t)try{return new URL(t).toString()}catch{return}}function R(t){let e=new URL(t.href),n={},r=ce(t.referrer);r&&(n.referrer=r);for(let i of j){let o=de(e.searchParams.get(i));o&&(n[i]=o)}return n}function N(t){let e=j.map(n=>t[n]??"").join("");return e.replaceAll("","")?e:void 0}var L=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu,Ie=/^[A-Za-z][A-Za-z0-9_.:-]*$/u,ge=/[\s\x00-\x1f\x7f]/u,Ae=/[\x00-\x1f\x7f]/u;function P(t){return new TextEncoder().encode(JSON.stringify(t)).byteLength}function w(t){return t.length>0&&t.length<=256&&!ge.test(t)}function F(t,e=2048){if(t.length>e)return!1;try{return new URL(t).origin.length>0}catch{return!1}}function C(t,e,n){return e>4?!1:typeof t=="string"?t.length<=1024:typeof t=="number"?Number.isFinite(t):t===null||typeof t=="boolean"?!0:Array.isArray(t)?t.length<=50&&t.every(r=>C(r,e+1,n)):Object.entries(t).every(([r,i])=>(n.count+=1,n.count<=100&&r.length>0&&r.length<=128&&!Ae.test(r)&&C(i,e+1,n)))}function J(t){let{context:e}=t;try{if(P(t)>16384)return!1}catch{return!1}return L.test(t.event_id)&&w(t.anonymous_id)&&w(t.session_id)&&(t.user_id===void 0||w(t.user_id))&&(t.event_type!=="event"||t.event_name.length<=100&&Ie.test(t.event_name)&&t.event_name!=="identify"&&t.event_name!=="page_view")&&F(e.url)&&e.path.startsWith("/")&&e.path.length<=1024&&(e.title===void 0||e.title.length<=512)&&(e.referrer===void 0||F(e.referrer))&&[e.utm_source,e.utm_medium,e.utm_campaign,e.utm_term,e.utm_content].every(n=>n===void 0||n.length>0&&n.length<=256)&&(t.properties===void 0||C(t.properties,0,{count:0}))}function $(t){if(!t||typeof t!="object")return;let e=t;if(!(e.receipt_version!==1||typeof e.receipt_id!="string"||!L.test(e.receipt_id)||typeof e.batch_id!="string"||!L.test(e.batch_id)||!Number.isInteger(e.accepted_events)||(e.accepted_events??0)<1||(e.accepted_events??0)>50||typeof e.accepted_at!="string"||Number.isNaN(Date.parse(e.accepted_at))))return e}function be(t,e){let n=new URL(t.href);return n.hash="",{path:n.pathname,url:n.toString(),...t.title?{title:t.title}:{},...e}}function q(t,e,n,r){let i={schema_version:1,event_id:t.randomUUID(),event_name:n.eventName,event_type:n.eventType,timestamp:new Date(t.now()).toISOString(),anonymous_id:e.anonymousId,session_id:e.sessionId,context:be(t.page(),e.attribution),...n.properties?{properties:n.properties}:{},...n.userId?{user_id:n.userId}:{}};if(!J(i)){r?.({code:"EVENT_INVALID",message:"Event was not queued because it failed the v1 contract."});return}return i}var Q=1;function Re(t){if(!t||typeof t!="object")return!1;let e=t;return e.version===Q&&typeof e.anonymousId=="string"&&typeof e.sessionId=="string"&&typeof e.lastActivityAt=="number"&&typeof e.attribution=="object"&&e.attribution!==null&&(e.userId===void 0||typeof e.userId=="string")}var v=class{#e;#i;#o;#t;#n;#s;constructor(e,n){this.#o=e,this.#i=n.onError,this.#t=n.sessionTimeoutMs,this.#e=`tracwell:${n.projectKey}:identity`,this.#n=n.persistence==="localStorage"?this.#h(e):void 0}identify(e,n){let r=this.#r(n);return r.userId&&r.userId!==e&&(r.anonymousId=this.#o.randomUUID(),r.sessionId=this.#o.randomUUID()),r.userId=e,this.#c(r),this.#l(r)}reset(){if(this.#s=void 0,this.#n)try{this.#n.removeItem(this.#e)}catch(e){this.#d(e)}}snapshot(e){return this.#l(this.#r(e))}#a(e){let n=this.#o.now();return{anonymousId:this.#o.randomUUID(),attribution:e,lastActivityAt:n,sessionId:this.#o.randomUUID(),version:Q}}#r(e){let n=this.#o.now(),r=this.#u();if(!r){let a=this.#a(e);return this.#c(a),a}let i=N(r.attribution),o=N(e);return(n-r.lastActivityAt>=this.#t||o!==void 0&&o!==i)&&(r.sessionId=this.#o.randomUUID(),r.attribution=e),r.lastActivityAt=n,this.#c(r),r}#u(){if(this.#s)return this.#s;if(this.#n)try{let e=this.#n.getItem(this.#e);if(!e)return;let n=JSON.parse(e);if(!Re(n)){this.#n.removeItem(this.#e);return}return this.#s=n,n}catch(e){this.#d(e);return}}#h(e){try{return e.storage()}catch(n){this.#d(n);return}}#d(e){this.#i?.({cause:e,code:"STORAGE_UNAVAILABLE",message:"Persistent identity storage is unavailable; using in-memory identity."})}#l(e){return{anonymousId:e.anonymousId,attribution:e.attribution,sessionId:e.sessionId,...e.userId?{userId:e.userId}:{}}}#c(e){if(this.#s=e,!!this.#n)try{this.#n.setItem(this.#e,JSON.stringify(e))}catch(n){this.#d(n)}}};var W=8634e4;function f(t,e,n,r){return{...r===void 0?{}:{cause:r},code:t,eventIds:n.batch.events.map(i=>i.event_id),message:e}}function Le(t){let e=JSON.parse(t??"{}");return e.version===1&&Array.isArray(e.events)?e.events.filter(n=>typeof n?.event_id=="string"&&typeof n.timestamp=="string"):[]}var S=class{#e;#i;#o;#t;#n;#s=!1;#a;#r=[];constructor(e,n){this.#i=e,this.#e=n,this.#o=n.collectionMode==="product"&&n.persistence==="localStorage"?`tracwell:${n.projectKey}:pending`:void 0}enqueue(e){if(this.#r.push(e),this.#s){this.#E(),this.#m();return}this.#r.length>=this.#e.batchSize&&this.flush()}restore(){let e=[];this.#f((r,i)=>{try{e=Le(r.getItem(i))}finally{r.removeItem(i)}});let n=this.#i.now();this.#r.unshift(...e.filter(r=>n-Date.parse(r.timestamp)<W)),this.#r.length&&this.flush()}clear(){this.#n?.(),this.#n=void 0,this.#t=void 0,this.#s=!1,this.#r=[],this.#f((e,n)=>e.removeItem(n))}flush(){return this.#a?this.#a:(this.#a=this.#u().finally(()=>{this.#a=void 0}),this.#a)}flushDue(){return this.#n?Promise.resolve():this.flush()}flushWithBeacon(){for(this.#n?.(),this.#n=void 0;this.#t||this.#r.length>0;){let e=this.#t??this.#h();if(!e)return;let n=JSON.stringify(e.batch);if(!this.#i.sendBeacon(this.#e.endpoint,n)){this.#e.onError?.(f("DELIVERY_FAILED","Browser lifecycle handoff was rejected by sendBeacon.",e));return}this.#t=void 0,this.#d({batchId:e.batch.batch_id,eventIds:e.batch.events.map(r=>r.event_id),status:"handed_off"})}}async#u(){this.#n?.(),this.#n=void 0;let e=this.#t??this.#h();if(!e)return;let n=await this.#c(e);n&&this.#r.length>0?await this.#u():n&&this.#_()}#h(){if(this.#r.length===0)return;let e=this.#r.slice(0,this.#e.batchSize),n;for(;e.length>0;){let r={batch_version:1,batch_id:this.#i.randomUUID(),project_key:this.#e.projectKey,sent_at:new Date(this.#i.now()).toISOString(),sdk:{name:"tracwell-browser",version:b},events:e};if(P(r)<=49152){n=r;break}e.pop()}if(!n){let r=this.#r.shift();r&&this.#e.onError?.({code:"EVENT_INVALID",eventIds:[r.event_id],message:"Event cannot fit in a valid browser batch."});return}return this.#r.splice(0,n.events.length),this.#t={attempt:0,batch:n},this.#t}#d(e){this.#e.onDelivery?.(e)}#l(e){let n=Math.min(Y*2**(e.attempt-1),G);this.#n=this.#i.setTimeout(()=>{this.#n=void 0,this.flush()},n)}async#c(e){let n;try{n=await this.#i.fetch(this.#e.endpoint,JSON.stringify(e.batch),"text/plain;charset=UTF-8")}catch(o){return this.#t!==e?!1:this.#p(e,o)}if(n.status!==202)return n.status===408||n.status===429||n.status>=500?this.#p(e,new Error(`Collector returned HTTP ${n.status}.`)):(this.#t===e&&(this.#t=void 0),this.#e.onError?.(f("DELIVERY_REJECTED",`Collector rejected the batch with non-retryable HTTP ${n.status}.`,e)),!1);let r;try{r=await n.json()}catch(o){return this.#e.onError?.(f("RECEIPT_INVALID","Collector returned an unreadable acceptance receipt.",e,o)),this.#p(e,o)}let i=$(r);return!i||i.batch_id!==e.batch.batch_id||i.accepted_events!==e.batch.events.length?(this.#e.onError?.(f("RECEIPT_INVALID","Collector receipt does not match the submitted batch.",e)),this.#p(e,new Error("Acceptance receipt mismatch."))):(this.#t===e&&(this.#t=void 0),this.#d({batchId:e.batch.batch_id,eventIds:e.batch.events.map(o=>o.event_id),receiptId:i.receipt_id,status:"accepted"}),!0)}#p(e,n){if(this.#t!==e)return!1;e.attempt+=1;let r=this.#i.now()-Date.parse(e.batch.events[0]?.timestamp??"")<W;return e.attempt<=this.#e.maxRetries&&r?(this.#s=!0,this.#e.onError?.(f("DELIVERY_FAILED","Event batch delivery failed; retrying.",e,n)),this.#E(),this.#m(),this.#l(e),!1):(this.#t=void 0,this.#e.onError?.(f("DELIVERY_EXHAUSTED","Event batch delivery exhausted its retries.",e,n)),this.#m(),!1)}#E(){let e=(this.#t?.batch.events.length??0)+this.#r.length,n=Math.min(e-A,this.#r.length);if(n<=0)return;let r=this.#r.splice(0,n);this.#e.onError?.({code:"DELIVERY_EXHAUSTED",eventIds:r.map(i=>i.event_id),message:"Retry buffer full; oldest events dropped."})}#_(){this.#s&&(this.#s=!1,this.#f((e,n)=>e.removeItem(n)))}#m(){let e=[...this.#t?.batch.events??[],...this.#r].slice(-A);this.#f((n,r)=>e.length?n.setItem(r,JSON.stringify({version:1,events:e})):n.removeItem(r))}#f(e){if(this.#o)try{let n=this.#i.storage();n&&e(n,this.#o)}catch{}}};function Z(t,e){let n=new v(t,e),r=new S(t,e),i=e.consent,o,h=!1,c=!1,a=[];function _(){return i==="granted"&&!(e.respectDoNotTrack&&t.doNotTrackEnabled())}function T(s,u,d,l){if(c||!_())return;let V=R(t.page()),U=n.snapshot(V),p=l??U.userId,ne=s==="identify"&&p?{eventName:"identify",eventType:"identify",...d?{properties:d}:{},userId:p}:s==="pageview"?{eventName:"page_view",eventType:"pageview",...d?{properties:d}:{},...p?{userId:p}:{}}:{eventName:u,eventType:"event",...d?{properties:d}:{},...p?{userId:p}:{}},m=q(t,U,ne,e.onError);if(m){if(s==="identify"&&l){let k=n.identify(l,V);m.anonymous_id=k.anonymousId,m.session_id=k.sessionId}return r.enqueue(m),m.event_id}}function I(s){if(!_())return;let u=new URL(t.page().href);u.hash="";let d=u.toString();if(d===o)return;let l=T("pageview","page_view",s);return l&&(o=d),l}function x(){a.forEach(s=>s()),a=[],h=!1}function D(){h||c||!_()||(h=!0,a=[t.addPageHideListener(()=>r.flushWithBeacon()),t.addVisibilityHiddenListener(()=>r.flushWithBeacon()),t.setInterval(()=>{r.flushDue()},e.flushIntervalMs)],r.restore(),e.autoTrackPageViews&&(a.push(t.subscribeToNavigation(()=>{I()})),I()))}return D(),{flush(){return r.flush()},getSession(){if(c||e.collectionMode!=="product"||!_())return;let s=n.snapshot(R(t.page()));return{anonymousId:s.anonymousId,sessionId:s.sessionId,...s.userId?{userId:s.userId}:{}}},identify(s,u){if(e.collectionMode==="private"){e.onError?.({code:"IDENTIFY_UNAVAILABLE",message:"identify() is unavailable in Private collection mode."});return}return T("identify","identify",u,s)},page:I,reset(){n.reset(),o=void 0},setConsent(s){if(i=s,s==="denied"){x(),r.clear(),n.reset(),o=void 0;return}D()},async shutdown(){c||(x(),await r.flush(),c=!0)},track(s,u){return T("event",s,u)}}}function ee(t,e){return window.addEventListener(t,e),()=>window.removeEventListener(t,e)}function te(){return{addPageHideListener(t){return ee("pagehide",t)},addVisibilityHiddenListener(t){let e=()=>{document.visibilityState==="hidden"&&t()};return document.addEventListener("visibilitychange",e),()=>document.removeEventListener("visibilitychange",e)},doNotTrackEnabled(){return navigator.doNotTrack==="1"},async fetch(t,e,n){return fetch(t,{body:e,credentials:"omit",headers:{"content-type":n},keepalive:!0,method:"POST"})},now(){return Date.now()},page(){return{href:window.location.href,referrer:document.referrer,title:document.title}},randomUUID(){return crypto.randomUUID()},sendBeacon(t,e){return navigator.sendBeacon(t,e)},setInterval(t,e){let n=window.setInterval(t,e);return()=>window.clearInterval(n)},setTimeout(t,e){let n=window.setTimeout(t,e);return()=>window.clearTimeout(n)},storage(){return window.localStorage},subscribeToNavigation(t){let e=!0,n=history.pushState,r=history.replaceState,i=()=>{e&&queueMicrotask(t)},o=function(...a){n.apply(history,a),i()},h=function(...a){r.apply(history,a),i()};history.pushState=o,history.replaceState=h;let c=ee("popstate",t);return()=>{e=!1,c(),history.pushState===o&&(history.pushState=n),history.replaceState===h&&(history.replaceState=r)}}}}function et(t){if(typeof window>"u"||typeof document>"u"||typeof navigator>"u")throw new Error("createTracwell must be called in a browser after the document is available.");return Z(te(),z(t))}export{H as DEFAULT_BATCH_SIZE,O as DEFAULT_COLLECTOR_ENDPOINT,B as DEFAULT_FLUSH_INTERVAL_MS,X as DEFAULT_MAX_RETRIES,K as DEFAULT_SESSION_TIMEOUT_MS,b as SDK_VERSION,et as createTracwell};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracwell",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Privacy-aware browser analytics and optional server-side AI crawler tracking for Tracwell",
5
5
  "license": "MIT",
6
6
  "type": "module",