@privatrak/tracker 0.1.0 → 1.0.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/CHANGELOG.md CHANGED
@@ -12,9 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
- Nothing has been published to npm yet; entries accumulate here until the first
16
- release. Until then these describe the shape of that release rather than a
17
- change anyone has to migrate.
15
+ ## [1.0.0] - 2026-09-21
16
+
17
+ The first supported release. `0.1.0` existed only to reserve the package name
18
+ and should not be depended on.
18
19
 
19
20
  ### Changed
20
21
 
package/README.md CHANGED
@@ -97,6 +97,27 @@ keep: `data-autocapture="pageviews,clicks"`, or `data-autocapture="none"` to
97
97
  record nothing automatically. Add `data-manual-init` to stop the script
98
98
  initialising itself, and call `window.tracker.init({ ... })` when you are ready.
99
99
 
100
+ ### Project settings, and what happens if they cannot be loaded
101
+
102
+ Settings come from three places, applied in this order: the values built into
103
+ the script, your project's settings fetched from `apiHost`, and the `data-`
104
+ attributes on the script tag, which win over both.
105
+
106
+ **Nothing is sent until that fetch has succeeded.** The project's settings carry
107
+ its URL redaction rules, so that one request is what stands between a URL in the
108
+ page and a URL in the database. It is retried five times — 500 ms, 1 s, 2 s and
109
+ 4 s apart — on network errors, timeouts, `408`, `429` and any `5xx`. A `401` or
110
+ `403` is not retried: the key is wrong, or the page's origin is not among the
111
+ project's allowed domains, and the answer will not change however often it is
112
+ asked.
113
+
114
+ If the settings never arrive, **the page load sends nothing at all**, and one
115
+ `console.warn` names the request that failed and says why. Sending unredacted
116
+ URLs would be worse than sending none. The [Tracker Reference][tracker-ref] goes
117
+ through the retry rules and the queue behaviour in full.
118
+
119
+ [tracker-ref]: https://privatrak.com/docs/tracker-reference
120
+
100
121
  ### The Element Picker
101
122
 
102
123
  Privatrak's point-and-click element picker is a second file,
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- var config={flush_interval_ms:5e3,max_queue_size:10,walk_depth:3,max_text_length:100,autocapture_pageviews:!0,autocapture:!0,autocapture_elements:["a","button","input[type=submit]","[role=button]","[role=link]","[role=menuitem]","[role=menuitemcheckbox]","[role=menuitemradio]","[role=option]","[role=tab]","[role=switch]","[role=checkbox]","[role=radio]","[role=combobox]"],pii_url_params:["email","token","key","password","secret"],normalize_path_ids:!0,pii_path_patterns:[],excluded_urls:[],sampling_rate:1},apiBase="",apiKey="",configLoaded=!1,trackingDisabled=!1,traits=[];function setApiBase(value){apiBase=value}function setApiKey(value){apiKey=value}function setConfigLoaded(value){configLoaded=value}function setTrackingDisabled(value){trackingDisabled=value}function setTraits(value){traits=value}function applyServerConfig(server){if(typeof server!="object"||server===null)return;let s=server;typeof s.flush_interval_ms=="number"&&(config.flush_interval_ms=s.flush_interval_ms),typeof s.max_queue_size=="number"&&(config.max_queue_size=s.max_queue_size),typeof s.walk_depth=="number"&&(config.walk_depth=s.walk_depth),typeof s.max_text_length=="number"&&(config.max_text_length=s.max_text_length),typeof s.autocapture_pageviews=="boolean"&&(config.autocapture_pageviews=s.autocapture_pageviews),typeof s.autocapture=="boolean"&&(config.autocapture=s.autocapture),Array.isArray(s.autocapture_elements)&&(config.autocapture_elements=s.autocapture_elements),Array.isArray(s.pii_url_params)&&(config.pii_url_params=s.pii_url_params),typeof s.normalize_path_ids=="boolean"&&(config.normalize_path_ids=s.normalize_path_ids),Array.isArray(s.pii_path_patterns)&&(config.pii_path_patterns=s.pii_path_patterns),Array.isArray(s.excluded_urls)&&(config.excluded_urls=s.excluded_urls),typeof s.sampling_rate=="number"&&(config.sampling_rate=s.sampling_rate)}async function fetchConfig(){try{let response=await fetch(`${apiBase}/api/config`,{headers:{"X-API-Key":apiKey},credentials:"omit"});if(!response.ok)return;let data=await response.json();applyServerConfig(data)}catch{}}function applyInitConfig(initConfig){initConfig.apiKey!==void 0&&(apiKey=initConfig.apiKey),initConfig.apiHost!==void 0&&(apiBase=initConfig.apiHost),initConfig.flushIntervalMs!==void 0&&(config.flush_interval_ms=initConfig.flushIntervalMs),initConfig.maxQueueSize!==void 0&&(config.max_queue_size=initConfig.maxQueueSize),initConfig.walkDepth!==void 0&&(config.walk_depth=initConfig.walkDepth),initConfig.maxTextLength!==void 0&&(config.max_text_length=initConfig.maxTextLength),initConfig.autocapturePageviews!==void 0&&(config.autocapture_pageviews=initConfig.autocapturePageviews),initConfig.autocapture!==void 0&&(config.autocapture=initConfig.autocapture),initConfig.autocaptureElements!==void 0&&(config.autocapture_elements=initConfig.autocaptureElements),initConfig.piiUrlParams!==void 0&&(config.pii_url_params=initConfig.piiUrlParams),typeof initConfig.normalizePathIds=="boolean"&&(config.normalize_path_ids=initConfig.normalizePathIds),Array.isArray(initConfig.piiPathPatterns)&&(config.pii_path_patterns=initConfig.piiPathPatterns),initConfig.excludedUrls!==void 0&&(config.excluded_urls=initConfig.excludedUrls),initConfig.samplingRate!==void 0&&(config.sampling_rate=initConfig.samplingRate),initConfig.traits!==void 0&&(traits=initConfig.traits)}var ID_PATTERNS=[/^\d+$/,/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,/^[0-9a-f]{8,}$/i];function isIdSegment(segment){return ID_PATTERNS.some(p=>p.test(segment))}function buildPiiPattern(){if(config.pii_url_params.length===0)return null;let escaped=config.pii_url_params.map(p=>p.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"));return new RegExp("("+escaped.join("|")+")","i")}function buildPiiPathPattern(){if(config.pii_path_patterns.length===0)return null;try{return new RegExp("^("+config.pii_path_patterns.join("|")+")$","i")}catch{return null}}function sanitizeUrl(url){try{let u=new URL(url),segments=u.pathname.split("/"),piiPathRegex=buildPiiPathPattern(),pathChanged=!1;for(let i=0;i<segments.length;i++){let seg=segments[i];if(seg){if(config.normalize_path_ids&&isIdSegment(seg)){segments[i]=":id",pathChanged=!0;continue}piiPathRegex?.test(seg)&&(segments[i]=":redacted",pathChanged=!0)}}pathChanged&&(u.pathname=segments.join("/"));let paramPattern=buildPiiPattern();if(paramPattern){let toRedact=[];u.searchParams.forEach((_v,k)=>{paramPattern.test(k)&&toRedact.push(k)});for(let k of toRedact)u.searchParams.set(k,":redacted")}return u.toString()}catch{return url}}function matchGlob(pattern,url){try{let path=new URL(url).pathname,escaped=pattern.replace(/[.+^${}()|[\]\\]/g,"\\$&");return new RegExp("^"+escaped.replace(/\*/g,".*")+"$").test(path)}catch{return!1}}function isExcludedUrl(url){return config.excluded_urls.some(pattern=>matchGlob(pattern,url))}function disableTracking(queue2){setTrackingDisabled(!0),queue2.length=0,config.autocapture_pageviews=!1,config.autocapture=!1}function applySampling(queue2){if(!(config.sampling_rate>=1)){if(config.sampling_rate<=0){disableTracking(queue2);return}Math.random()>config.sampling_rate&&disableTracking(queue2)}}function extractText(el){let raw=el.textContent;if(!raw)return;let trimmed=raw.trim().replace(/\s+/g," ");if(trimmed.length!==0)return trimmed.length>config.max_text_length?trimmed.slice(0,config.max_text_length):trimmed}function extractDataTrack(el){let current=el;for(let i=0;i<=config.walk_depth&&current;i++){let trackValue=current.getAttribute("data-track");if(trackValue!==null){let attrs={};for(let attr of current.attributes)if(attr.name.startsWith("data-track-")){let key=attr.name.slice(11);attrs[key]=attr.value}return{dataTrack:trackValue||void 0,dataTrackAttrs:Object.keys(attrs).length>0?attrs:void 0}}current=current.parentElement}return{dataTrack:void 0,dataTrackAttrs:void 0}}function getSegment(el){let seg=el.tagName.toLowerCase();el instanceof HTMLInputElement&&el.type&&(seg+='[type="'+CSS.escape(el.type)+'"]');let role=el.getAttribute("role");role&&(seg+='[role="'+CSS.escape(role)+'"]');let parent=el.parentElement;if(parent){let tag=el.tagName,siblings=parent.children,sameTag=0,idx=0;for(let i=0;i<siblings.length;i++){let sib=siblings[i];sib&&sib.tagName===tag&&(sameTag++,sib===el&&(idx=sameTag))}sameTag>1&&(seg+=":nth-of-type("+String(idx)+")")}return seg}function computeSelector(target){let parts=[],el=target;for(;el&&el!==document.documentElement;)parts.unshift(getSegment(el)),el=el.parentElement;let result=parts.join(" > ");for(;result.length>200&&parts.length>1;)parts.shift(),result=parts.join(" > ");return result}function getGenericSegment(el){let seg=el.tagName.toLowerCase();el instanceof HTMLInputElement&&el.type&&(seg+='[type="'+CSS.escape(el.type)+'"]');let role=el.getAttribute("role");return role&&(seg+='[role="'+CSS.escape(role)+'"]'),seg}function computeGenericSelector(target){let parts=[],el=target;for(;el&&el!==document.documentElement;)parts.unshift(getGenericSegment(el)),el=el.parentElement;let result=parts.join(" > ");for(;result.length>200&&parts.length>1;)parts.shift(),result=parts.join(" > ");let specific=computeSelector(target);return result===specific?null:result}function createPageviewEvent(referrer){return{event_type:"pageview",page_url:sanitizeUrl(window.location.href),referrer:referrer?sanitizeUrl(referrer):void 0,timestamp:new Date().toISOString()}}function createClickEvent(el){let{dataTrack,dataTrackAttrs}=extractDataTrack(el);return{event_type:"click",page_url:sanitizeUrl(window.location.href),element_tag:el.tagName.toLowerCase(),element_selector:computeSelector(el),element_selector_generalized:computeGenericSelector(el)??void 0,element_text:extractText(el),data_track:dataTrack,data_track_attrs:dataTrackAttrs,timestamp:new Date().toISOString()}}function createFormSubmitEvent(form){let{dataTrack,dataTrackAttrs}=extractDataTrack(form);return{event_type:"form_submit",page_url:sanitizeUrl(window.location.href),element_tag:form.tagName.toLowerCase(),element_selector:computeSelector(form),element_selector_generalized:computeGenericSelector(form)??void 0,data_track:dataTrack,data_track_attrs:dataTrackAttrs,timestamp:new Date().toISOString()}}var pickerActive=!1,pickerDashboardOrigin=null;function setPickerActive(value){pickerActive=value}function setPickerDashboardOrigin(value){pickerDashboardOrigin=value}var REJECTION_REASONS={401:"the API key is missing or not valid",403:"this site's origin is not in the project's allowed domains",429:"the project is over its event limit"},queue=[],lastUrl="",isFlushing=!1;function setLastUrl(value){lastUrl=value}function filterEvents(events){return events.filter(event=>!(isExcludedUrl(event.page_url)||event.event_type==="pageview"&&!config.autocapture_pageviews||(event.event_type==="click"||event.event_type==="form_submit")&&!config.autocapture&&!event.data_track))}function enqueue(event){trackingDisabled||pickerActive||apiKey&&(queue.length>=100||(queue.push(event),configLoaded&&queue.length>=config.max_queue_size&&flush()))}async function flush(){if(trackingDisabled||pickerActive||isFlushing||queue.length===0)return;isFlushing=!0;let events=filterEvents(queue.splice(0));if(events.length===0){isFlushing=!1;return}let body={events,...traits.length>0&&{traits}},headers={"Content-Type":"application/json"};apiKey&&(headers["X-API-Key"]=apiKey);try{let response=await fetch(`${apiBase}/api/events`,{method:"POST",headers,body:JSON.stringify(body),keepalive:!0,credentials:"omit"});if(!response.ok){let status=response.status;status===401||status===403||status===429?(console.warn(`[tracker] The API rejected events with HTTP ${String(status)} \u2014 ${REJECTION_REASONS[status]??"the request was refused"}. Tracking is now off for this page.`),disableTracking(queue)):status>=500&&queue.unshift(...events)}}catch{queue.unshift(...events)}finally{isFlushing=!1}}function flushBeacon(){if(trackingDisabled||queue.length===0)return;let events=filterEvents(queue.splice(0));if(events.length===0)return;let body={events,...traits.length>0&&{traits}};apiKey&&(body.api_key=apiKey);let headers={"Content-Type":"application/json"};apiKey&&(headers["X-API-Key"]=apiKey),fetch(`${apiBase}/api/events`,{method:"POST",headers,body:JSON.stringify(body),keepalive:!0,credentials:"omit"}).catch(()=>{})}function isSensitive(el){if(el instanceof HTMLInputElement){let t=el.type.toLowerCase();if(t==="password"||t==="hidden")return!0}let ac=el.getAttribute("autocomplete");return!!(ac&&ac.toLowerCase().startsWith("cc-"))}function isInteractive(el){if(el.hasAttribute("data-track"))return!0;if(!config.autocapture)return!1;for(let selector of config.autocapture_elements)try{if(el.matches(selector))return!0}catch{}return!1}function findInteractiveAncestor(target){let el=target;for(let i=0;i<=config.walk_depth&&el;i++){if(isInteractive(el))return el;el=el.parentElement}return null}function isPickerTarget(el){return isInteractive(el)||el.matches("form")}function findPickerTarget(target){let el=target;for(let i=0;i<=config.walk_depth&&el;i++){if(isPickerTarget(el))return el;el=el.parentElement}return null}var PICKER_HANDOFF_KEY="__stPickerHandoff";var PICKER_FILE="tracker-picker.js",picker=null,loading=!1,failed=!1,pendingOptions=null,pickerHostApi={setActive:setPickerActive,setDashboardOrigin:setPickerDashboardOrigin,clearQueue:()=>{queue.length=0},computeSelector,computeGenericSelector,extractText,extractDataTrack,findPickerTarget};function pickerScriptUrl(){for(let script of document.querySelectorAll("script[src]")){let src=script.getAttribute("src");if(src?.includes("tracker.js"))try{return new URL(PICKER_FILE,new URL(src,document.baseURI)).toString()}catch{}}return apiBase?apiBase.replace(/\/+$/,"")+"/"+PICKER_FILE:"/"+PICKER_FILE}function registerPicker(module){if(picker=module,loading=!1,pendingOptions){let options=pendingOptions;pendingOptions=null,activate(module,options)}}function activate(module,options){module.activate({...options,dashboardOrigin:pickerDashboardOrigin})}function abandonLoad(url){loading=!1,failed=!0,pendingOptions=null,setPickerActive(!1),console.warn(`[tracker] Could not load the Element Picker from ${url}. Tracking is unaffected.`)}function loadPicker(){let url=pickerScriptUrl(),script=document.createElement("script");script.src=url,script.async=!0,script[PICKER_HANDOFF_KEY]={host:pickerHostApi,register:registerPicker},script.addEventListener("load",()=>{script.remove(),picker||abandonLoad(url)}),script.addEventListener("error",()=>{script.remove(),abandonLoad(url)}),document.head.appendChild(script)}function requestPicker(options){if(picker){activate(picker,options??{});return}failed||(setPickerActive(!0),queue.length=0,pendingOptions=options??{},!loading&&(loading=!0,loadPicker()))}function onPickerNavigation(){picker?.onNavigation()}function onPickerWindowMessage(e){if(picker){picker.handleMessage(e);return}typeof e.data!="object"||e.data===null||e.data.type!=="st-picker-activate"||pickerDashboardOrigin&&e.origin!==pickerDashboardOrigin||pickerActive||!window.opener||requestPicker()}function onClickCapture(e){if(pickerActive)return;let target=e.target;if(!(target instanceof Element))return;let interactive=findInteractiveAncestor(target);interactive&&(isSensitive(interactive)||enqueue(createClickEvent(interactive)))}function onSubmitCapture(e){if(pickerActive)return;let form=e.target;form instanceof HTMLFormElement&&(isSensitive(form)||!config.autocapture&&!extractDataTrack(form).dataTrack||enqueue(createFormSubmitEvent(form)))}function onSpaNavigation(){if(pickerActive){onPickerNavigation();return}if(!config.autocapture_pageviews)return;let currentUrl=window.location.href;if(currentUrl===lastUrl)return;let referrer=lastUrl;setLastUrl(currentUrl),enqueue(createPageviewEvent(referrer))}function patchHistory(){let origPushState=history.pushState.bind(history),origReplaceState=history.replaceState.bind(history);history.pushState=function(...args){origPushState(...args),onSpaNavigation()},history.replaceState=function(...args){origReplaceState(...args),onSpaNavigation()}}var version="0.1.0",initialized=!1,storedInitConfig={};function init(initConfig){if(typeof window>"u")return;if(initialized){console.warn("[tracker] Already initialized. Ignoring duplicate init() call.");return}if(initialized=!0,storedInitConfig=initConfig??{},storedInitConfig.apiKey!==void 0&&setApiKey(storedInitConfig.apiKey),storedInitConfig.apiHost!==void 0&&setApiBase(storedInitConfig.apiHost),!apiKey){console.error("[tracker] Missing apiKey. Events will not be sent.");return}applyInitConfig(storedInitConfig),setLastUrl(window.location.href),config.autocapture_pageviews&&enqueue(createPageviewEvent(document.referrer)),patchHistory(),window.addEventListener("popstate",onSpaNavigation),document.addEventListener("click",onClickCapture),document.addEventListener("submit",onSubmitCapture),window.addEventListener("message",onPickerWindowMessage);let hashMatch=window.location.hash.match(/^#st-picker=(.+)$/);if(hashMatch?.[1]){let origin=decodeURIComponent(hashMatch[1]);setPickerDashboardOrigin(origin);try{sessionStorage.setItem("st-picker",JSON.stringify({active:!0,origin}))}catch{}history.replaceState(null,"",window.location.pathname+window.location.search),window.opener&&requestPicker()}else try{let stored=sessionStorage.getItem("st-picker");if(stored&&window.opener){let parsed=JSON.parse(stored);if(typeof parsed=="object"&&parsed!==null&&"active"in parsed&&"origin"in parsed){let obj=parsed;if(obj.active&&typeof obj.origin=="string"){setPickerDashboardOrigin(obj.origin);let storedScan="scanEnabled"in obj&&obj.scanEnabled===!0,storedNav="navigateMode"in obj&&obj.navigateMode===!0,rawMode="selectorMode"in obj?obj.selectorMode:null;requestPicker({scanEnabled:storedScan,navigateMode:storedNav,selectorMode:rawMode==="generic"||rawMode==="exact"?rawMode:null})}}}}catch{}document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&flushBeacon()}),fetchConfig().then(()=>{applyInitConfig(storedInitConfig),applySampling(queue),!trackingDisabled&&(setConfigLoaded(!0),setInterval(()=>{flush()},config.flush_interval_ms),flush())})}function track(name,attrs){typeof window>"u"||trackingDisabled||name&&enqueue({event_type:"custom",page_url:sanitizeUrl(window.location.href),data_track:name,data_track_attrs:attrs,timestamp:new Date().toISOString()})}function setTraits2(newTraits){typeof window>"u"||setTraits(newTraits)}export{init,setTraits2 as setTraits,track,version};
1
+ var config={flush_interval_ms:5e3,max_queue_size:10,walk_depth:3,max_text_length:100,autocapture_pageviews:!0,autocapture:!0,autocapture_elements:["a","button","input[type=submit]","[role=button]","[role=link]","[role=menuitem]","[role=menuitemcheckbox]","[role=menuitemradio]","[role=option]","[role=tab]","[role=switch]","[role=checkbox]","[role=radio]","[role=combobox]"],pii_url_params:["email","token","key","password","secret"],normalize_path_ids:!0,pii_path_patterns:[],excluded_urls:[],sampling_rate:1},apiBase="",apiKey="",configLoaded=!1,trackingDisabled=!1,traits=[];function setApiBase(value){apiBase=value}function setApiKey(value){apiKey=value}function setConfigLoaded(value){configLoaded=value}function setTrackingDisabled(value){trackingDisabled=value}function setTraits(value){traits=value}function applyServerConfig(server){if(typeof server!="object"||server===null)return;let s=server;typeof s.flush_interval_ms=="number"&&(config.flush_interval_ms=s.flush_interval_ms),typeof s.max_queue_size=="number"&&(config.max_queue_size=s.max_queue_size),typeof s.walk_depth=="number"&&(config.walk_depth=s.walk_depth),typeof s.max_text_length=="number"&&(config.max_text_length=s.max_text_length),typeof s.autocapture_pageviews=="boolean"&&(config.autocapture_pageviews=s.autocapture_pageviews),typeof s.autocapture=="boolean"&&(config.autocapture=s.autocapture),Array.isArray(s.autocapture_elements)&&(config.autocapture_elements=s.autocapture_elements),Array.isArray(s.pii_url_params)&&(config.pii_url_params=s.pii_url_params),typeof s.normalize_path_ids=="boolean"&&(config.normalize_path_ids=s.normalize_path_ids),Array.isArray(s.pii_path_patterns)&&(config.pii_path_patterns=s.pii_path_patterns),Array.isArray(s.excluded_urls)&&(config.excluded_urls=s.excluded_urls),typeof s.sampling_rate=="number"&&(config.sampling_rate=s.sampling_rate)}var CONFIG_FETCH_ATTEMPTS=5,CONFIG_RETRY_DELAYS_MS=[500,1e3,2e3,4e3],CONFIG_FETCH_TIMEOUT_MS=3e3;function isRetryableStatus(status){return status===408||status===429||status>=500}function delay(ms){return new Promise(resolve=>setTimeout(resolve,ms))}async function attemptConfigFetch(){let controller=new AbortController,timer=setTimeout(()=>{controller.abort()},CONFIG_FETCH_TIMEOUT_MS);try{let response=await fetch(`${apiBase}/api/config`,{headers:{"X-API-Key":apiKey},credentials:"omit",signal:controller.signal});if(!response.ok)return isRetryableStatus(response.status)?{kind:"retry",status:response.status}:{kind:"permanent",status:response.status};let data=await response.json();return applyServerConfig(data),{kind:"ok"}}catch{return{kind:"retry"}}finally{clearTimeout(timer)}}async function fetchConfig(){let status;for(let attempt=0;attempt<CONFIG_FETCH_ATTEMPTS;attempt++){let outcome=await attemptConfigFetch();if(outcome.kind==="ok")return{loaded:!0,attempts:attempt+1};if(outcome.kind==="permanent")return{loaded:!1,attempts:attempt+1,status:outcome.status};status=outcome.status;let wait=CONFIG_RETRY_DELAYS_MS[attempt];if(wait===void 0)break;await delay(wait)}return{loaded:!1,attempts:CONFIG_FETCH_ATTEMPTS,...status!==void 0&&{status}}}var CONFIG_FAILURE_REASONS={401:"the API key is missing or not valid",403:"this site's origin is not in the project's allowed domains"};function describeConfigFailure(result){let attempts=result.attempts===1?"1 attempt":`${String(result.attempts)} attempts`;if(result.status===void 0)return`no response after ${attempts} (network error or timeout)`;let reason=CONFIG_FAILURE_REASONS[result.status],http=`HTTP ${String(result.status)}`;return reason===void 0?`${http} after ${attempts}`:`${http} after ${attempts} (${reason})`}function applyInitConfig(initConfig){initConfig.apiKey!==void 0&&(apiKey=initConfig.apiKey),initConfig.apiHost!==void 0&&(apiBase=initConfig.apiHost),initConfig.flushIntervalMs!==void 0&&(config.flush_interval_ms=initConfig.flushIntervalMs),initConfig.maxQueueSize!==void 0&&(config.max_queue_size=initConfig.maxQueueSize),initConfig.walkDepth!==void 0&&(config.walk_depth=initConfig.walkDepth),initConfig.maxTextLength!==void 0&&(config.max_text_length=initConfig.maxTextLength),initConfig.autocapturePageviews!==void 0&&(config.autocapture_pageviews=initConfig.autocapturePageviews),initConfig.autocapture!==void 0&&(config.autocapture=initConfig.autocapture),initConfig.autocaptureElements!==void 0&&(config.autocapture_elements=initConfig.autocaptureElements),initConfig.piiUrlParams!==void 0&&(config.pii_url_params=initConfig.piiUrlParams),typeof initConfig.normalizePathIds=="boolean"&&(config.normalize_path_ids=initConfig.normalizePathIds),Array.isArray(initConfig.piiPathPatterns)&&(config.pii_path_patterns=initConfig.piiPathPatterns),initConfig.excludedUrls!==void 0&&(config.excluded_urls=initConfig.excludedUrls),initConfig.samplingRate!==void 0&&(config.sampling_rate=initConfig.samplingRate),initConfig.traits!==void 0&&(traits=initConfig.traits)}var ID_PATTERNS=[/^\d+$/,/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,/^[0-9a-f]{8,}$/i];function isIdSegment(segment){return ID_PATTERNS.some(p=>p.test(segment))}function buildPiiPattern(){if(config.pii_url_params.length===0)return null;let escaped=config.pii_url_params.map(p=>p.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"));return new RegExp("("+escaped.join("|")+")","i")}function buildPiiPathPattern(){if(config.pii_path_patterns.length===0)return null;try{return new RegExp("^("+config.pii_path_patterns.join("|")+")$","i")}catch{return null}}function sanitizeUrl(url){try{let u=new URL(url),segments=u.pathname.split("/"),piiPathRegex=buildPiiPathPattern(),pathChanged=!1;for(let i=0;i<segments.length;i++){let seg=segments[i];if(seg){if(config.normalize_path_ids&&isIdSegment(seg)){segments[i]=":id",pathChanged=!0;continue}piiPathRegex?.test(seg)&&(segments[i]=":redacted",pathChanged=!0)}}pathChanged&&(u.pathname=segments.join("/"));let paramPattern=buildPiiPattern();if(paramPattern){let toRedact=[];u.searchParams.forEach((_v,k)=>{paramPattern.test(k)&&toRedact.push(k)});for(let k of toRedact)u.searchParams.set(k,":redacted")}return u.toString()}catch{return url}}function matchGlob(pattern,url){try{let path=new URL(url).pathname,escaped=pattern.replace(/[.+^${}()|[\]\\]/g,"\\$&");return new RegExp("^"+escaped.replace(/\*/g,".*")+"$").test(path)}catch{return!1}}function isExcludedUrl(url){return config.excluded_urls.some(pattern=>matchGlob(pattern,url))}function disableTracking(queue2){setTrackingDisabled(!0),queue2.length=0,config.autocapture_pageviews=!1,config.autocapture=!1}function applySampling(queue2){if(!(config.sampling_rate>=1)){if(config.sampling_rate<=0){disableTracking(queue2);return}Math.random()>config.sampling_rate&&disableTracking(queue2)}}function extractText(el){let raw=el.textContent;if(!raw)return;let trimmed=raw.trim().replace(/\s+/g," ");if(trimmed.length!==0)return trimmed.length>config.max_text_length?trimmed.slice(0,config.max_text_length):trimmed}function extractDataTrack(el){let current=el;for(let i=0;i<=config.walk_depth&&current;i++){let trackValue=current.getAttribute("data-track");if(trackValue!==null){let attrs={};for(let attr of current.attributes)if(attr.name.startsWith("data-track-")){let key=attr.name.slice(11);attrs[key]=attr.value}return{dataTrack:trackValue||void 0,dataTrackAttrs:Object.keys(attrs).length>0?attrs:void 0}}current=current.parentElement}return{dataTrack:void 0,dataTrackAttrs:void 0}}function getSegment(el){let seg=el.tagName.toLowerCase();el instanceof HTMLInputElement&&el.type&&(seg+='[type="'+CSS.escape(el.type)+'"]');let role=el.getAttribute("role");role&&(seg+='[role="'+CSS.escape(role)+'"]');let parent=el.parentElement;if(parent){let tag=el.tagName,siblings=parent.children,sameTag=0,idx=0;for(let i=0;i<siblings.length;i++){let sib=siblings[i];sib&&sib.tagName===tag&&(sameTag++,sib===el&&(idx=sameTag))}sameTag>1&&(seg+=":nth-of-type("+String(idx)+")")}return seg}function computeSelector(target){let parts=[],el=target;for(;el&&el!==document.documentElement;)parts.unshift(getSegment(el)),el=el.parentElement;let result=parts.join(" > ");for(;result.length>200&&parts.length>1;)parts.shift(),result=parts.join(" > ");return result}function getGenericSegment(el){let seg=el.tagName.toLowerCase();el instanceof HTMLInputElement&&el.type&&(seg+='[type="'+CSS.escape(el.type)+'"]');let role=el.getAttribute("role");return role&&(seg+='[role="'+CSS.escape(role)+'"]'),seg}function computeGenericSelector(target){let parts=[],el=target;for(;el&&el!==document.documentElement;)parts.unshift(getGenericSegment(el)),el=el.parentElement;let result=parts.join(" > ");for(;result.length>200&&parts.length>1;)parts.shift(),result=parts.join(" > ");let specific=computeSelector(target);return result===specific?null:result}function createPageviewEvent(referrer){return{event_type:"pageview",page_url:window.location.href,referrer:referrer===""?void 0:referrer,timestamp:new Date().toISOString()}}function createClickEvent(el){let{dataTrack,dataTrackAttrs}=extractDataTrack(el);return{event_type:"click",page_url:window.location.href,element_tag:el.tagName.toLowerCase(),element_selector:computeSelector(el),element_selector_generalized:computeGenericSelector(el)??void 0,element_text:extractText(el),data_track:dataTrack,data_track_attrs:dataTrackAttrs,timestamp:new Date().toISOString()}}function createFormSubmitEvent(form){let{dataTrack,dataTrackAttrs}=extractDataTrack(form);return{event_type:"form_submit",page_url:window.location.href,element_tag:form.tagName.toLowerCase(),element_selector:computeSelector(form),element_selector_generalized:computeGenericSelector(form)??void 0,data_track:dataTrack,data_track_attrs:dataTrackAttrs,timestamp:new Date().toISOString()}}var pickerActive=!1,pickerDashboardOrigin=null;function setPickerActive(value){pickerActive=value}function setPickerDashboardOrigin(value){pickerDashboardOrigin=value}var REJECTION_REASONS={401:"the API key is missing or not valid",403:"this site's origin is not in the project's allowed domains",429:"the project is over its event limit"},queue=[],lastUrl="",isFlushing=!1;function setLastUrl(value){lastUrl=value}function sanitizeEvent(event){let pageUrl=sanitizeUrl(event.page_url),referrer=event.referrer===void 0?void 0:sanitizeUrl(event.referrer);return pageUrl===event.page_url&&referrer===event.referrer?event:{...event,page_url:pageUrl,...referrer!==void 0&&{referrer}}}function prepareEvents(events){return filterEvents(events.map(sanitizeEvent))}function filterEvents(events){return events.filter(event=>!(isExcludedUrl(event.page_url)||event.event_type==="pageview"&&!config.autocapture_pageviews||(event.event_type==="click"||event.event_type==="form_submit")&&!config.autocapture&&!event.data_track))}function enqueue(event){trackingDisabled||pickerActive||apiKey&&(queue.length>=100||(queue.push(event),configLoaded&&queue.length>=config.max_queue_size&&flush()))}async function flush(){if(trackingDisabled||pickerActive||!configLoaded||isFlushing||queue.length===0)return;isFlushing=!0;let events=prepareEvents(queue.splice(0));if(events.length===0){isFlushing=!1;return}let body={events,...traits.length>0&&{traits}},headers={"Content-Type":"application/json"};apiKey&&(headers["X-API-Key"]=apiKey);try{let response=await fetch(`${apiBase}/api/events`,{method:"POST",headers,body:JSON.stringify(body),keepalive:!0,credentials:"omit"});if(!response.ok){let status=response.status;status===401||status===403||status===429?(console.warn(`[tracker] The API rejected events with HTTP ${String(status)} \u2014 ${REJECTION_REASONS[status]??"the request was refused"}. Tracking is now off for this page.`),disableTracking(queue)):status>=500&&queue.unshift(...events)}}catch{queue.unshift(...events)}finally{isFlushing=!1}}function flushBeacon(){if(trackingDisabled||!configLoaded||queue.length===0)return;let events=prepareEvents(queue.splice(0));if(events.length===0)return;let body={events,...traits.length>0&&{traits}};apiKey&&(body.api_key=apiKey);let headers={"Content-Type":"application/json"};apiKey&&(headers["X-API-Key"]=apiKey),fetch(`${apiBase}/api/events`,{method:"POST",headers,body:JSON.stringify(body),keepalive:!0,credentials:"omit"}).catch(()=>{})}function isSensitive(el){if(el instanceof HTMLInputElement){let t=el.type.toLowerCase();if(t==="password"||t==="hidden")return!0}let ac=el.getAttribute("autocomplete");return!!(ac&&ac.toLowerCase().startsWith("cc-"))}function isInteractive(el){if(el.hasAttribute("data-track"))return!0;if(!config.autocapture)return!1;for(let selector of config.autocapture_elements)try{if(el.matches(selector))return!0}catch{}return!1}function findInteractiveAncestor(target){let el=target;for(let i=0;i<=config.walk_depth&&el;i++){if(isInteractive(el))return el;el=el.parentElement}return null}function isPickerTarget(el){return isInteractive(el)||el.matches("form")}function findPickerTarget(target){let el=target;for(let i=0;i<=config.walk_depth&&el;i++){if(isPickerTarget(el))return el;el=el.parentElement}return null}var PICKER_HANDOFF_KEY="__stPickerHandoff";var PICKER_FILE="tracker-picker.js",picker=null,loading=!1,failed=!1,pendingOptions=null,pickerHostApi={setActive:setPickerActive,setDashboardOrigin:setPickerDashboardOrigin,clearQueue:()=>{queue.length=0},computeSelector,computeGenericSelector,extractText,extractDataTrack,findPickerTarget};function pickerScriptUrl(){for(let script of document.querySelectorAll("script[src]")){let src=script.getAttribute("src");if(src?.includes("tracker.js"))try{return new URL(PICKER_FILE,new URL(src,document.baseURI)).toString()}catch{}}return apiBase?apiBase.replace(/\/+$/,"")+"/"+PICKER_FILE:"/"+PICKER_FILE}function registerPicker(module){if(picker=module,loading=!1,pendingOptions){let options=pendingOptions;pendingOptions=null,activate(module,options)}}function activate(module,options){module.activate({...options,dashboardOrigin:pickerDashboardOrigin})}function abandonLoad(url){loading=!1,failed=!0,pendingOptions=null,setPickerActive(!1),console.warn(`[tracker] Could not load the Element Picker from ${url}. Tracking is unaffected.`)}function loadPicker(){let url=pickerScriptUrl(),script=document.createElement("script");script.src=url,script.async=!0,script[PICKER_HANDOFF_KEY]={host:pickerHostApi,register:registerPicker},script.addEventListener("load",()=>{script.remove(),picker||abandonLoad(url)}),script.addEventListener("error",()=>{script.remove(),abandonLoad(url)}),document.head.appendChild(script)}function requestPicker(options){if(picker){activate(picker,options??{});return}failed||(setPickerActive(!0),queue.length=0,pendingOptions=options??{},!loading&&(loading=!0,loadPicker()))}function onPickerNavigation(){picker?.onNavigation()}function onPickerWindowMessage(e){if(picker){picker.handleMessage(e);return}typeof e.data!="object"||e.data===null||e.data.type!=="st-picker-activate"||pickerDashboardOrigin&&e.origin!==pickerDashboardOrigin||pickerActive||!window.opener||requestPicker()}function onClickCapture(e){if(pickerActive)return;let target=e.target;if(!(target instanceof Element))return;let interactive=findInteractiveAncestor(target);interactive&&(isSensitive(interactive)||enqueue(createClickEvent(interactive)))}function onSubmitCapture(e){if(pickerActive)return;let form=e.target;form instanceof HTMLFormElement&&(isSensitive(form)||!config.autocapture&&!extractDataTrack(form).dataTrack||enqueue(createFormSubmitEvent(form)))}function onSpaNavigation(){if(pickerActive){onPickerNavigation();return}if(!config.autocapture_pageviews)return;let currentUrl=window.location.href;if(currentUrl===lastUrl)return;let referrer=lastUrl;setLastUrl(currentUrl),enqueue(createPageviewEvent(referrer))}function patchHistory(){let origPushState=history.pushState.bind(history),origReplaceState=history.replaceState.bind(history);history.pushState=function(...args){origPushState(...args),onSpaNavigation()},history.replaceState=function(...args){origReplaceState(...args),onSpaNavigation()}}var version="1.0.0",initialized=!1,storedInitConfig={};function init(initConfig){if(typeof window>"u")return;if(initialized){console.warn("[tracker] Already initialized. Ignoring duplicate init() call.");return}if(initialized=!0,storedInitConfig=initConfig??{},storedInitConfig.apiKey!==void 0&&setApiKey(storedInitConfig.apiKey),storedInitConfig.apiHost!==void 0&&setApiBase(storedInitConfig.apiHost),!apiKey){console.error("[tracker] Missing apiKey. Events will not be sent.");return}applyInitConfig(storedInitConfig),setLastUrl(window.location.href),config.autocapture_pageviews&&enqueue(createPageviewEvent(document.referrer)),patchHistory(),window.addEventListener("popstate",onSpaNavigation),document.addEventListener("click",onClickCapture),document.addEventListener("submit",onSubmitCapture),window.addEventListener("message",onPickerWindowMessage);let hashMatch=window.location.hash.match(/^#st-picker=(.+)$/);if(hashMatch?.[1]){let origin=decodeURIComponent(hashMatch[1]);setPickerDashboardOrigin(origin);try{sessionStorage.setItem("st-picker",JSON.stringify({active:!0,origin}))}catch{}history.replaceState(null,"",window.location.pathname+window.location.search),window.opener&&requestPicker()}else try{let stored=sessionStorage.getItem("st-picker");if(stored&&window.opener){let parsed=JSON.parse(stored);if(typeof parsed=="object"&&parsed!==null&&"active"in parsed&&"origin"in parsed){let obj=parsed;if(obj.active&&typeof obj.origin=="string"){setPickerDashboardOrigin(obj.origin);let storedScan="scanEnabled"in obj&&obj.scanEnabled===!0,storedNav="navigateMode"in obj&&obj.navigateMode===!0,rawMode="selectorMode"in obj?obj.selectorMode:null;requestPicker({scanEnabled:storedScan,navigateMode:storedNav,selectorMode:rawMode==="generic"||rawMode==="exact"?rawMode:null})}}}}catch{}document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&flushBeacon()}),fetchConfig().then(result=>{if(!result.loaded){failClosed(result);return}applyInitConfig(storedInitConfig),applySampling(queue),!trackingDisabled&&(setConfigLoaded(!0),setInterval(()=>{flush()},config.flush_interval_ms),flush())})}function failClosed(result){console.warn(`[tracker] Could not load this project's settings from ${apiBase}/api/config \u2014 ${describeConfigFailure(result)}. No events were sent from this page load: without those settings the tracker cannot apply the project's URL redaction rules, and sending unredacted URLs would be worse than sending nothing.`),disableTracking(queue)}function track(name,attrs){typeof window>"u"||trackingDisabled||name&&enqueue({event_type:"custom",page_url:window.location.href,data_track:name,data_track_attrs:attrs,timestamp:new Date().toISOString()})}function setTraits2(newTraits){typeof window>"u"||setTraits(newTraits)}export{init,setTraits2 as setTraits,track,version};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privatrak/tracker",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "Cookieless, privacy-first analytics tracker: auto-captures pageviews, clicks and form submissions with no cookies, no localStorage, no PII and no consent banner.",
5
5
  "keywords": [
6
6
  "analytics",
@@ -41,7 +41,8 @@
41
41
  ".": {
42
42
  "types": "./dist/index.d.mts",
43
43
  "import": "./dist/index.mjs"
44
- }
44
+ },
45
+ "./package.json": "./package.json"
45
46
  },
46
47
  "engines": {
47
48
  "node": ">=18"
@@ -56,11 +57,13 @@
56
57
  "dev": "esbuild src/bootstrap.ts --bundle --format=iife --define:__TRACKER_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/tracker.js --watch & esbuild src/picker/entry.ts --bundle --format=iife --define:__TRACKER_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/tracker-picker.js --watch & wait",
57
58
  "lint": "eslint --max-warnings 0 .",
58
59
  "typecheck": "tsc --noEmit",
60
+ "test": "node --experimental-strip-types --import ./test/register-ts-resolve.mjs --test 'src/**/*.test.ts'",
59
61
  "format": "prettier --write 'src/**/*.ts'",
60
62
  "format:check": "prettier --check 'src/**/*.ts'",
61
63
  "generate": "openapi-typescript ../openapi.yaml -o src/generated/api-types.ts"
62
64
  },
63
65
  "devDependencies": {
66
+ "@types/node": "^20.19.43",
64
67
  "esbuild": "^0.25",
65
68
  "eslint": "^9",
66
69
  "openapi-typescript": "^7",