tracwell 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tracwell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # SDK
2
+
3
+ Browser analytics SDK for page views, custom events, identity, batching, and
4
+ bounded delivery retries.
5
+
6
+ ## Script installation
7
+
8
+ The production browser artifact is served from the existing Collector domain:
9
+
10
+ ```html
11
+ <script
12
+ defer
13
+ src="https://collect.tracwell.app/script.js"
14
+ data-project-key="tw_live_..."
15
+ data-collection-mode="private"
16
+ data-consent="granted"
17
+ data-respect-do-not-track="true"
18
+ ></script>
19
+ ```
20
+
21
+ Loading the script captures the initial page view and exposes the same client
22
+ as `window.tracwell` for custom events:
23
+
24
+ ```js
25
+ window.tracwell?.track("signup_completed", { plan: "starter" });
26
+ ```
27
+
28
+ The script derives `/v1/events` from its own origin, so the same artifact works
29
+ against the loopback Collector during local development. The production bundle
30
+ reports SDK version `0.1.0`, targets ES2022-capable browsers, and has a 4.6 KB
31
+ gzip budget enforced by
32
+ `build:cdn`.
33
+
34
+ ## Public API
35
+
36
+ Install the npm package:
37
+
38
+ ```sh
39
+ pnpm add tracwell
40
+ ```
41
+
42
+ ```ts
43
+ import { createTracwell } from "tracwell";
44
+
45
+ const tracwell = createTracwell({
46
+ collectionMode: "private",
47
+ projectKey: "tw_live_...",
48
+ });
49
+
50
+ tracwell.track("signup_completed", { plan: "starter" });
51
+ tracwell.identify("customer_123");
52
+ ```
53
+
54
+ `createTracwell()` starts collection immediately in the browser and captures
55
+ the initial page view by default. It must be called after the document is
56
+ available, not during server rendering.
57
+
58
+ ## Responsibilities
59
+
60
+ - Generate stable event and batch IDs before delivery.
61
+ - Capture initial page views, SPA navigation, referrer, UTM, and tagged-link
62
+ context.
63
+ - Expose typed custom-event and identity APIs.
64
+ - Batch only against the limits exported by `@tracwell/contracts`.
65
+ - Retry bounded transient failures without changing event IDs.
66
+ - Respect configured consent and Do Not Track behavior before collection.
67
+
68
+ The SDK must remain browser-only, small, non-blocking, and unable to set trusted
69
+ collector fields. It must not contain secrets or connect to ClickHouse.
70
+
71
+ ## Identity and attribution
72
+
73
+ Collection mode is explicit:
74
+
75
+ - `private` is the default. It never reads or writes cookies, `localStorage`,
76
+ or `sessionStorage`; `identify()` reports `IDENTIFY_UNAVAILABLE`. Anonymous
77
+ and session values exist only in memory until the collector replaces them.
78
+ - `product` persists first-party anonymous, session, and optional user identity
79
+ in `localStorage`. Configure consent before collection where required.
80
+ - Private mode rejects an explicit `persistence: "localStorage"` instead of
81
+ silently weakening the project's privacy behavior.
82
+
83
+ - `anonymous_id` is first-party and persists per project unless persistence is
84
+ disabled or identity is reset.
85
+ - `session_id` rotates after 30 minutes without a captured event.
86
+ - A different non-empty UTM campaign starts a new session immediately.
87
+ - The first external referrer and UTM values are preserved for the session.
88
+ - The SDK preserves the absolute referrer. Trusted ClickHouse row mapping uses
89
+ registered-domain/public-suffix parsing so sibling subdomains are not
90
+ reported as external acquisition.
91
+ - `identify()` validates the supplied opaque user ID before associating it with
92
+ future events.
93
+ - Hash-only URL changes do not create duplicate SPA page views.
94
+ - URL fragments are excluded from captured URLs and `path` contains only the
95
+ pathname.
96
+
97
+ Product identity uses `localStorage`, never fingerprinting. If storage is
98
+ disabled or unavailable, the SDK explicitly reports `STORAGE_UNAVAILABLE`
99
+ through `onError` and uses in-memory identity for the page lifetime.
100
+
101
+ ## Delivery semantics
102
+
103
+ - Events receive lightweight client preflight validation before entering
104
+ memory. The Collector remains the canonical Zod validation boundary for all
105
+ untrusted browser input.
106
+ - Normal cross-origin delivery uses a safelisted `text/plain` JSON body so it
107
+ does not add a CORS preflight request; the collector still parses and
108
+ validates the body as JSON.
109
+ - Batches default to 25 events and are reduced when necessary to remain within
110
+ the shared 48 KiB limit.
111
+ - Normal delivery reports `accepted` only after a matching `202` receipt
112
+ confirms the batch ID and event count.
113
+ - Network errors, `408`, `429`, and `5xx` responses use bounded in-memory retry
114
+ without changing event or batch IDs.
115
+ - Other `4xx` responses report `DELIVERY_REJECTED` and are not retried.
116
+ - Page-hide and hidden-document delivery prefers `sendBeacon`; a successful
117
+ browser handoff reports `handed_off`, never `accepted`.
118
+ - Exhausted delivery is surfaced through `onError` with the affected event IDs.
119
+
120
+ The default collector endpoint is
121
+ `https://collect.tracwell.app/v1/events`. Production overrides must use HTTPS;
122
+ loopback HTTP is allowed only for local development.
123
+
124
+ ## Privacy controls
125
+
126
+ - `consent: "required"` prevents identity creation and collection until
127
+ `setConsent("granted")`.
128
+ - Revoking consent stops listeners, removes queued events, and resets identity.
129
+ - Do Not Track is respected by default and can be disabled explicitly per
130
+ customer configuration.
131
+ - `persistence: "none"` keeps all identifiers in memory.
132
+ - Private collection forces `persistence: "none"` and blocks `identify()`.
133
+
134
+ ## Structure
135
+
136
+ ```text
137
+ src/
138
+ index.ts Stable public exports and browser-only factory
139
+ script.ts Lean self-starting CDN entry and `window.tracwell` exposure
140
+ script-config.ts Strict script-dataset parsing and normalized CDN defaults
141
+ client.ts Consent-aware API and lifecycle orchestration
142
+ config.ts Defaults and configuration validation
143
+ identity.ts Anonymous, session, and user identity state
144
+ attribution.ts First-touch-per-session UTM and referrer capture
145
+ events.ts Context creation and contract-validated events
146
+ transport.ts Size-aware batching, receipts, beacon, and retries
147
+ preflight.ts Small client-side event and receipt checks
148
+ runtime.ts Browser API adapter
149
+ types.ts Public configuration, client, delivery, and error types
150
+ client.test.ts Deterministic SDK behavior and failure tests
151
+ scripts/
152
+ build-cdn.mjs Minified browser build and gzip-size enforcement
153
+ build-npm.mjs Self-contained ESM package build
154
+ smoke-npm.mjs Packed runtime and TypeScript consumer smoke test
155
+ static/
156
+ _headers Cross-origin browser asset headers
157
+ ```
158
+
159
+ Keep browser globals isolated in `runtime.ts`, keep `index.ts` as the stable
160
+ public API, and test behavior through an injected deterministic runtime.
161
+
162
+ ## Dependencies
163
+
164
+ - `@tracwell/contracts` for inferred types and shared limits
165
+ - Browser platform APIs for navigation, lifecycle, and delivery
166
+ - `esbuild` at build time for the minified browser artifact
167
+
168
+ The published npm artifact bundles its runtime contract constants and ships
169
+ generated declarations, so consumers do not need the private contracts
170
+ workspace. Compile-time compatibility checks keep the duplicated public
171
+ property and issue types aligned with the canonical contracts package.
172
+
173
+ The npm package is distributed under the MIT License. Publishing the package
174
+ does not make the private application repository public.
175
+
176
+ ## Verify
177
+
178
+ ```sh
179
+ pnpm --filter {packages/sdk} typecheck
180
+ pnpm --filter {packages/sdk} test
181
+ pnpm --filter {packages/sdk} build:cdn
182
+ pnpm --filter {packages/sdk} check:package
183
+ ```
@@ -0,0 +1,11 @@
1
+ import type { NormalizedConfig, SdkError, TracwellConfig } from "./types";
2
+ export declare const DEFAULT_COLLECTOR_ENDPOINT = "https://collect.tracwell.app/v1/events";
3
+ export declare const DEFAULT_BATCH_SIZE = 25;
4
+ export declare const DEFAULT_FLUSH_INTERVAL_MS = 10000;
5
+ export declare const DEFAULT_MAX_RETRIES = 2;
6
+ export declare const DEFAULT_SESSION_TIMEOUT_MS: number;
7
+ export declare const SDK_VERSION = "0.1.0";
8
+ export declare function normalizeProjectKey(value: string, onError?: (error: SdkError) => void): string;
9
+ export declare function normalizeCollectorEndpoint(value: string, onError?: (error: SdkError) => void): string;
10
+ export declare function normalizeCollectorUrl(endpoint: URL, onError?: (error: SdkError) => void): string;
11
+ export declare function normalizeConfig(config: TracwellConfig): NormalizedConfig;
@@ -0,0 +1,3 @@
1
+ export { DEFAULT_BATCH_SIZE, DEFAULT_COLLECTOR_ENDPOINT, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_MAX_RETRIES, DEFAULT_SESSION_TIMEOUT_MS, SDK_VERSION, } from "./config";
2
+ export type { ContractIssue, ContractIssueCode, ConsentState, DeliveryReport, EventProperties, JsonPrimitive, JsonValue, SdkError, SdkErrorCode, TracwellClient, TracwellConfig, } from "./types";
3
+ export declare function createTracwell(config: import("./types").TracwellConfig): import("./types").TracwellClient;
@@ -0,0 +1 @@
1
+ var D="https://collect.tracwell.app/v1/events",k=25,U=1e4,O=2,H=1800*1e3,b="0.1.0";function m(e,t){throw t?.({code:"CONFIG_INVALID",message:e}),new TypeError(e)}function tt(e,t){let n=e.trim();return n.length<20||n.length>128||!/^[A-Za-z0-9_-]+$/u.test(n)?m("projectKey does not match the public project-key contract.",t):n}function et(e,t){let n;try{n=new URL(e)}catch{return m("endpoint must be an absolute HTTPS URL.",t)}return nt(n,t)}function nt(e,t){let n=e.hostname==="localhost"||e.hostname==="127.0.0.1"||e.hostname==="[::1]";return e.protocol!=="https:"&&!n?m("endpoint must use HTTPS unless it uses a loopback host.",t):e.toString()}function E(e,t,n,r,i){return!Number.isSafeInteger(t)||t<n||t>r?m(`${e} must be an integer between ${n} and ${r}.`,i):t}function B(e){let t=tt(e.projectKey,e.onError),n=e.collectionMode??"private",r=et(e.endpoint??D,e.onError);return n==="private"&&e.persistence!==void 0&&e.persistence!=="none"?m('Private collection requires persistence: "none".',e.onError):{autoTrackPageViews:e.autoTrackPageViews??!0,batchSize:E("batchSize",e.batchSize??k,1,50,e.onError),consent:e.consent??"granted",collectionMode:n,endpoint:r,flushIntervalMs:E("flushIntervalMs",e.flushIntervalMs??U,1e3,6e4,e.onError),maxRetries:E("maxRetries",e.maxRetries??O,0,5,e.onError),persistence:n==="private"?"none":e.persistence??"localStorage",projectKey:t,respectDoNotTrack:e.respectDoNotTrack??!0,sessionTimeoutMs:E("sessionTimeoutMs",e.sessionTimeoutMs??H,6e4,1440*60*1e3,e.onError),...e.onDelivery?{onDelivery:e.onDelivery}:{},...e.onError?{onError:e.onError}:{}}}var X=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"];function rt(e){let t=e?.trim();return t||void 0}function it(e){if(e)try{return new URL(e).toString()}catch{return}}function G(e){let t=new URL(e.href),n={},r=it(e.referrer);r&&(n.referrer=r);for(let i of X){let o=rt(t.searchParams.get(i));o&&(n[i]=o)}return n}function A(e){let t=X.map(n=>e[n]??"").join("");return t.replaceAll("","")?t:void 0}var N=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu,_t=/^[A-Za-z][A-Za-z0-9_.:-]*$/u,yt=/[\s\x00-\x1f\x7f]/u,Tt=/[\x00-\x1f\x7f]/u;function L(e){return new TextEncoder().encode(JSON.stringify(e)).byteLength}function R(e){return e.length>0&&e.length<=256&&!yt.test(e)}function Y(e,t=2048){if(e.length>t)return!1;try{return new URL(e).origin.length>0}catch{return!1}}function w(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=>w(r,t+1,n)):Object.entries(e).every(([r,i])=>(n.count+=1,n.count<=100&&r.length>0&&r.length<=128&&!Tt.test(r)&&w(i,t+1,n)))}function K(e){let{context:t}=e;try{if(L(e)>16384)return!1}catch{return!1}return N.test(e.event_id)&&R(e.anonymous_id)&&R(e.session_id)&&(e.user_id===void 0||R(e.user_id))&&(e.event_type!=="event"||e.event_name.length<=100&&_t.test(e.event_name)&&e.event_name!=="identify"&&e.event_name!=="page_view")&&Y(t.url)&&t.path.startsWith("/")&&t.path.length<=1024&&(t.title===void 0||t.title.length<=512)&&(t.referrer===void 0||Y(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||w(e.properties,0,{count:0}))}function z(e){if(!e||typeof e!="object")return;let t=e;if(!(t.receipt_version!==1||typeof t.receipt_id!="string"||!N.test(t.receipt_id)||typeof t.batch_id!="string"||!N.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 St(e,t){let n=new URL(e.href);return n.hash="",{path:n.pathname,url:n.toString(),...e.title?{title:e.title}:{},...t}}function j(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:St(e.page(),t.attribution),...n.properties?{properties:n.properties}:{},...n.userId?{user_id:n.userId}:{}};if(!K(i)){r?.({code:"EVENT_INVALID",message:"Event was not queued because it failed the v1 contract."});return}return i}var F=1;function gt(e){if(!e||typeof e!="object")return!1;let t=e;return t.version===F&&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 _=class{#t;#o;#e;#i;#r;#n;constructor(t,n){this.#e=t,this.#o=n.onError,this.#i=n.sessionTimeoutMs,this.#t=`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=t,this.#h(r),this.#s(r)}reset(){if(this.#n=void 0,this.#r)try{this.#r.removeItem(this.#t)}catch(t){this.#a(t)}}snapshot(t){return this.#s(this.#d(t))}#c(t){let n=this.#e.now();return{anonymousId:this.#e.randomUUID(),attribution:t,lastActivityAt:n,sessionId:this.#e.randomUUID(),version:F}}#d(t){let n=this.#e.now(),r=this.#u();if(!r){let a=this.#c(t);return this.#h(a),a}let i=A(r.attribution),o=A(t);return(n-r.lastActivityAt>=this.#i||o!==void 0&&o!==i)&&(r.sessionId=this.#e.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.#t);if(!t)return;let n=JSON.parse(t);if(!gt(n)){this.#r.removeItem(this.#t);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.#t,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 y=class{#t;#o;#e;#i;#r;#n=[];constructor(t,n){this.#o=t,this.#t=n}enqueue(t){this.#n.push(t),this.#n.length>=this.#t.batchSize&&this.flush()}clear(){this.#i?.(),this.#i=void 0,this.#e=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.#e||this.#n.length>0;){let t=this.#e??this.#d();if(!t)return;let n=JSON.stringify(t.batch);if(!this.#o.sendBeacon(this.#t.endpoint,n)){this.#t.onError?.(f("DELIVERY_FAILED","Browser lifecycle handoff was rejected by sendBeacon.",t));return}this.#e=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.#e??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.#t.batchSize),n;for(;t.length>0;){let r={batch_version:1,batch_id:this.#o.randomUUID(),project_key:this.#t.projectKey,sent_at:new Date(this.#o.now()).toISOString(),sdk:{name:"tracwell-browser",version:b},events:t};if(L(r)<=49152){n=r;break}t.pop()}if(!n){let r=this.#n.shift();r&&this.#t.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.#e={attempt:0,batch:n},this.#e}#u(t){this.#t.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.#t.endpoint,JSON.stringify(t.batch),"text/plain;charset=UTF-8")}catch(o){return this.#e!==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.#e===t&&(this.#e=void 0),this.#t.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.#t.onError?.(f("RECEIPT_INVALID","Collector returned an unreadable acceptance receipt.",t,o)),this.#s(t,o)}let i=z(r);return!i||i.batch_id!==t.batch.batch_id||i.accepted_events!==t.batch.events.length?(this.#t.onError?.(f("RECEIPT_INVALID","Collector receipt does not match the submitted batch.",t)),this.#s(t,new Error("Acceptance receipt mismatch."))):(this.#e===t&&(this.#e=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.#e!==t?!1:(t.attempt+=1,t.attempt<=this.#t.maxRetries?(this.#t.onError?.(f("DELIVERY_FAILED","Event batch delivery failed and is scheduled for bounded retry.",t,n)),this.#p(t),!1):(this.#e=void 0,this.#t.onError?.(f("DELIVERY_EXHAUSTED","Event batch delivery exhausted the configured retry limit.",t,n)),!1))}};function J(e,t){let n=new _(e,t),r=new y(e,t),i=t.consent,o,u=!1,p=!1,a=[];function T(){return i==="granted"&&!(t.respectDoNotTrack&&e.doNotTrackEnabled())}function S(s,c,d,h){if(p||!T())return;let V=G(e.page()),M=n.snapshot(V),l=h??M.userId,W=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:c,eventType:"event",...d?{properties:d}:{},...l?{userId:l}:{}},v=j(e,M,W,t.onError);if(v)return s==="identify"&&h&&n.identify(h,V),r.enqueue(v),v.event_id}function g(s){if(!T())return;let c=new URL(e.page().href);c.hash="";let d=c.toString();if(d===o)return;let h=S("pageview","page_view",s);return h&&(o=d),h}function P(){a.forEach(s=>s()),a=[],u=!1}function x(){u||p||!T()||(u=!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 x(),{flush(){return r.flush()},identify(s,c){if(t.collectionMode==="private"){t.onError?.({code:"IDENTIFY_UNAVAILABLE",message:"identify() is unavailable in Private collection mode."});return}return S("identify","identify",c,s)},page:g,reset(){n.reset(),o=void 0},setConsent(s){if(i=s,s==="denied"){P(),r.clear(),n.reset(),o=void 0;return}x()},async shutdown(){p||(P(),await r.flush(),p=!0)},track(s,c){return S("event",s,c)}}}function $(e,t){return window.addEventListener(e,t),()=>window.removeEventListener(e,t)}function q(){return{addPageHideListener(e){return $("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()},u=function(...a){r.apply(history,a),i()};history.pushState=o,history.replaceState=u;let p=$("popstate",e);return()=>{t=!1,p(),history.pushState===o&&(history.pushState=n),history.replaceState===u&&(history.replaceState=r)}}}}function Ft(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 J(q(),B(e))}export{k as DEFAULT_BATCH_SIZE,D as DEFAULT_COLLECTOR_ENDPOINT,U as DEFAULT_FLUSH_INTERVAL_MS,O as DEFAULT_MAX_RETRIES,H as DEFAULT_SESSION_TIMEOUT_MS,b as SDK_VERSION,Ft as createTracwell};
@@ -0,0 +1,66 @@
1
+ export type JsonPrimitive = boolean | number | string | null;
2
+ export type JsonValue = JsonPrimitive | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ export type EventProperties = Record<string, JsonValue>;
6
+ export type ContractIssueCode = "DUPLICATE_EVENT_ID" | "INVALID_FORMAT" | "INVALID_PAYLOAD" | "INVALID_TYPE" | "INVALID_VALUE" | "PAYLOAD_TOO_LARGE" | "PROPERTY_COUNT_EXCEEDED" | "PROPERTY_DEPTH_EXCEEDED" | "RESERVED_EVENT_NAME" | "TIMESTAMP_TOO_FAR_IN_FUTURE" | "TIMESTAMP_TOO_OLD" | "UNKNOWN_FIELD" | "VALUE_TOO_LARGE" | "VALUE_TOO_SMALL";
7
+ export interface ContractIssue {
8
+ code: ContractIssueCode;
9
+ path: (number | string)[];
10
+ message: string;
11
+ }
12
+ export type ConsentState = "denied" | "granted" | "required";
13
+ export type CollectionMode = "private" | "product";
14
+ export type SdkErrorCode = "CONFIG_INVALID" | "DELIVERY_EXHAUSTED" | "DELIVERY_FAILED" | "DELIVERY_REJECTED" | "EVENT_INVALID" | "IDENTIFY_UNAVAILABLE" | "RECEIPT_INVALID" | "STORAGE_UNAVAILABLE";
15
+ export interface SdkError {
16
+ code: SdkErrorCode;
17
+ message: string;
18
+ cause?: unknown;
19
+ eventIds?: string[];
20
+ issues?: ContractIssue[];
21
+ }
22
+ export interface DeliveryReport {
23
+ batchId: string;
24
+ eventIds: string[];
25
+ status: "accepted" | "handed_off";
26
+ receiptId?: string;
27
+ }
28
+ export interface TracwellConfig {
29
+ projectKey: string;
30
+ collectionMode?: CollectionMode;
31
+ endpoint?: string;
32
+ autoTrackPageViews?: boolean;
33
+ batchSize?: number;
34
+ consent?: ConsentState;
35
+ flushIntervalMs?: number;
36
+ maxRetries?: number;
37
+ persistence?: "localStorage" | "none";
38
+ respectDoNotTrack?: boolean;
39
+ sessionTimeoutMs?: number;
40
+ onDelivery?: (report: DeliveryReport) => void;
41
+ onError?: (error: SdkError) => void;
42
+ }
43
+ export interface TracwellClient {
44
+ flush(): Promise<void>;
45
+ identify(userId: string, properties?: EventProperties): string | undefined;
46
+ page(properties?: EventProperties): string | undefined;
47
+ reset(): void;
48
+ setConsent(consent: Exclude<ConsentState, "required">): void;
49
+ shutdown(): Promise<void>;
50
+ track(eventName: string, properties?: EventProperties): string | undefined;
51
+ }
52
+ export interface NormalizedConfig {
53
+ autoTrackPageViews: boolean;
54
+ batchSize: number;
55
+ consent: ConsentState;
56
+ collectionMode: CollectionMode;
57
+ endpoint: string;
58
+ flushIntervalMs: number;
59
+ maxRetries: number;
60
+ persistence: "localStorage" | "none";
61
+ projectKey: string;
62
+ respectDoNotTrack: boolean;
63
+ sessionTimeoutMs: number;
64
+ onDelivery?: (report: DeliveryReport) => void;
65
+ onError?: (error: SdkError) => void;
66
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "tracwell",
3
+ "version": "0.1.0",
4
+ "description": "Small, privacy-aware browser analytics SDK for Tracwell",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/npm/index.js",
9
+ "module": "./dist/npm/index.js",
10
+ "types": "./dist/npm/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/npm/index.d.ts",
14
+ "import": "./dist/npm/index.js",
15
+ "default": "./dist/npm/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist/npm/index.js",
20
+ "dist/npm/index.d.ts",
21
+ "dist/npm/config.d.ts",
22
+ "dist/npm/types.d.ts",
23
+ "LICENSE",
24
+ "README.md"
25
+ ],
26
+ "keywords": [
27
+ "analytics",
28
+ "browser",
29
+ "privacy",
30
+ "tracwell",
31
+ "web-analytics"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "devDependencies": {
37
+ "esbuild": "0.28.1",
38
+ "publint": "0.3.24",
39
+ "vitest": "4.1.10",
40
+ "@tracwell/contracts": "0.0.0"
41
+ },
42
+ "scripts": {
43
+ "build": "pnpm build:npm && pnpm build:cdn",
44
+ "build:cdn": "node scripts/build-cdn.mjs",
45
+ "build:npm": "node scripts/build-npm.mjs && tsc -p tsconfig.build.json",
46
+ "check:package": "pnpm build:npm && publint && node scripts/smoke-npm.mjs",
47
+ "test": "vitest run",
48
+ "typecheck": "tsc -p tsconfig.json"
49
+ }
50
+ }