tracwell 0.1.0 → 0.3.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
@@ -13,12 +13,33 @@ The production browser artifact is served from the existing Collector domain:
13
13
  src="https://collect.tracwell.app/script.js"
14
14
  data-project-key="tw_live_..."
15
15
  data-collection-mode="private"
16
- data-consent="granted"
17
- data-respect-do-not-track="true"
18
16
  ></script>
19
17
  ```
20
18
 
21
- Loading the script captures the initial page view and exposes the same client
19
+ ### Optional script attributes
20
+
21
+ | Attribute | Values | Default |
22
+ | --- | --- | --- |
23
+ | `data-project-key` | Your public project key (never a server key) | Required |
24
+ | `data-collection-mode` | `private`, `product` | `private` |
25
+ | `data-consent` | `granted`, `required`, `denied` | `granted` |
26
+ | `data-respect-do-not-track` | `true`, `false` | `true` |
27
+
28
+ Keep collection mode explicit and aligned with website settings. The generated
29
+ snippet omits default consent and Do Not Track attributes. Add
30
+ `data-consent="required"` to wait for consent before identity creation or event
31
+ collection. After the script loads, forward the visitor's saved choice and
32
+ subsequent changes using `window.tracwell?.setConsent("granted")` or
33
+ `window.tracwell?.setConsent("denied")`. An early call before the script loads
34
+ has no effect; initialize the consent integration after SDK load.
35
+
36
+ Only these four data attributes are supported. Use the npm SDK for other
37
+ initialization options, including batching, persistence overrides, and a custom
38
+ collector URL. The script reads attributes at load time and does not fetch
39
+ configuration from the dashboard; update the installed snippet when changing
40
+ website settings.
41
+
42
+ When consent and Do Not Track permit collection, loading the script captures the initial page view and exposes the same client
22
43
  as `window.tracwell` for custom events:
23
44
 
24
45
  ```js
@@ -27,7 +48,7 @@ window.tracwell?.track("signup_completed", { plan: "starter" });
27
48
 
28
49
  The script derives `/v1/events` from its own origin, so the same artifact works
29
50
  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
51
+ reports SDK version `0.2.0`, targets ES2022-capable browsers, and has a 4.7 KB
31
52
  gzip budget enforced by
32
53
  `build:cdn`.
33
54
 
@@ -49,12 +70,32 @@ const tracwell = createTracwell({
49
70
 
50
71
  tracwell.track("signup_completed", { plan: "starter" });
51
72
  tracwell.identify("customer_123");
73
+
74
+ const session = tracwell.getSession();
52
75
  ```
53
76
 
54
77
  `createTracwell()` starts collection immediately in the browser and captures
55
78
  the initial page view by default. It must be called after the document is
56
79
  available, not during server rendering.
57
80
 
81
+ ## Framework compatibility
82
+
83
+ The npm SDK is a framework-neutral ESM browser library. It has no React, Vue,
84
+ Svelte, Angular, router, or rendering-framework dependency. Importing it during
85
+ SSR is safe; call `createTracwell()` from that framework's browser mount or
86
+ hydration lifecycle after `document` is available.
87
+
88
+ Multi-page applications capture a page view each time the browser loads the
89
+ application. Single-page applications using the standard History API receive
90
+ automatic page views for `pushState`, `replaceState`, and back/forward
91
+ navigation. This covers the default path-based routers in modern web
92
+ frameworks. Hash fragments remain intentionally excluded from analytics URLs;
93
+ hash-only navigation does not create a page view.
94
+
95
+ Framework adapters should own only lifecycle integration and dependency
96
+ injection. Event validation, identity, attribution, navigation observation,
97
+ batching, retries, consent, and delivery remain in this core package.
98
+
58
99
  ## Responsibilities
59
100
 
60
101
  - Generate stable event and batch IDs before delivery.
@@ -65,8 +106,9 @@ available, not during server rendering.
65
106
  - Retry bounded transient failures without changing event IDs.
66
107
  - Respect configured consent and Do Not Track behavior before collection.
67
108
 
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.
109
+ The default SDK entry must remain browser-only, small, non-blocking, and unable
110
+ to set trusted collector fields. It must not contain secrets or connect to
111
+ ClickHouse. The separate server entry accepts a server-side key as documented below.
70
112
 
71
113
  ## Identity and attribution
72
114
 
@@ -90,6 +132,17 @@ Collection mode is explicit:
90
132
  reported as external acquisition.
91
133
  - `identify()` validates the supplied opaque user ID before associating it with
92
134
  future events.
135
+ - Identifying a different user on the same installation rotates both anonymous
136
+ identity and session before recording the new identity. Call `reset()` on
137
+ logout so an anonymous post-logout visit also starts cleanly.
138
+ - `getSession()` returns the active Product-mode `anonymousId`, `sessionId`,
139
+ and optional `userId` so a server-created checkout can carry the same
140
+ identifiers into an authenticated revenue event. It returns `undefined` in
141
+ Private mode, without consent, when Do Not Track blocks collection, or after
142
+ shutdown.
143
+ - Treat the returned IDs as attribution context, not authorization. Revenue
144
+ `user_id` must come from the authenticated account or verified payment
145
+ webhook rather than a client-supplied value.
93
146
  - Hash-only URL changes do not create duplicate SPA page views.
94
147
  - URL fragments are excluded from captured URLs and `path` contains only the
95
148
  pathname.
@@ -148,6 +201,7 @@ src/
148
201
  runtime.ts Browser API adapter
149
202
  types.ts Public configuration, client, delivery, and error types
150
203
  client.test.ts Deterministic SDK behavior and failure tests
204
+ index.test.ts SSR import and browser-initialization boundary
151
205
  scripts/
152
206
  build-cdn.mjs Minified browser build and gzip-size enforcement
153
207
  build-npm.mjs Self-contained ESM package build
@@ -181,3 +235,25 @@ pnpm --filter {packages/sdk} test
181
235
  pnpm --filter {packages/sdk} build:cdn
182
236
  pnpm --filter {packages/sdk} check:package
183
237
  ```
238
+
239
+ ## Server-only AI crawler entry
240
+
241
+ `tracwell/server` exports `trackAiCrawler(request, { serverKey, statusCode?,
242
+ collectorUrl? })`. Import only on your backend; never expose `tw_sk_`
243
+ keys in browser code. This separate bundle adds nothing to the default browser
244
+ entry or CDN script. Available starting with npm package version 0.3.0. The unchanged browser
245
+ transport continues reporting browser SDK version 0.2.0.
246
+
247
+ Call from Next.js proxy/middleware or another server/edge handler. Pass the
248
+ promise to the hosting runtime's `waitUntil` (and inspect its result), or await
249
+ on a persistent server. Keep existing request handling intact. The helper ignores
250
+ ordinary traffic, API paths and common static assets. Robots, sitemap and Markdown
251
+ requests remain trackable. Handler/cache placement determines coverage.
252
+
253
+ Results are `ignored`, `accepted` (matching Queue receipt), or `failed` with a
254
+ non-sensitive error code. Three total attempts use the same event ID, timestamp
255
+ and sanitized URL. Retries are in memory only. Do not treat acceptance as confirmed
256
+ database persistence. Queries/fragments are stripped; no IP, cookies or other
257
+ request headers are sent. Raw UA is sent solely for collector classification and
258
+ is not stored. Current provider matches are user-agent based, not IP verified.
259
+ See the public `/docs/ai-crawlers` guide for setup and direct POST integration.
@@ -4,7 +4,7 @@ export declare const DEFAULT_BATCH_SIZE = 25;
4
4
  export declare const DEFAULT_FLUSH_INTERVAL_MS = 10000;
5
5
  export declare const DEFAULT_MAX_RETRIES = 2;
6
6
  export declare const DEFAULT_SESSION_TIMEOUT_MS: number;
7
- export declare const SDK_VERSION = "0.1.0";
7
+ export declare const SDK_VERSION = "0.2.0";
8
8
  export declare function normalizeProjectKey(value: string, onError?: (error: SdkError) => void): string;
9
9
  export declare function normalizeCollectorEndpoint(value: string, onError?: (error: SdkError) => void): string;
10
10
  export declare function normalizeCollectorUrl(endpoint: URL, onError?: (error: SdkError) => void): string;
@@ -1,3 +1,3 @@
1
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";
2
+ export type { ContractIssue, ContractIssueCode, ConsentState, DeliveryReport, EventProperties, JsonPrimitive, JsonValue, SdkError, SdkErrorCode, TracwellClient, TracwellConfig, TracwellSession, } from "./types";
3
3
  export declare function createTracwell(config: import("./types").TracwellConfig): import("./types").TracwellClient;
package/dist/npm/index.js CHANGED
@@ -1 +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};
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};
@@ -0,0 +1,18 @@
1
+ export interface CrawlerTrackingOptions {
2
+ /** Secret tw_sk_ key. Never include in browser code. */
3
+ serverKey: string;
4
+ collectorUrl?: string;
5
+ statusCode?: number;
6
+ }
7
+ export type CrawlerTrackingResult = {
8
+ status: "ignored";
9
+ } | {
10
+ status: "accepted";
11
+ eventId: string;
12
+ } | {
13
+ status: "failed";
14
+ eventId: string;
15
+ code: "CONFIGURATION" | "DELIVERY_REJECTED" | "DELIVERY_UNAVAILABLE";
16
+ };
17
+ /** Await on a persistent server, or pass the promise to the runtime's waitUntil. */
18
+ export declare function trackAiCrawler(request: Request, options: CrawlerTrackingOptions): Promise<CrawlerTrackingResult>;
@@ -0,0 +1 @@
1
+ var l=[{token:"ChatGPT-User",provider:"OpenAI",category:"answers"},{token:"OAI-SearchBot",provider:"OpenAI",category:"indexing"},{token:"GPTBot",provider:"OpenAI",category:"training"},{token:"Claude-User",provider:"Anthropic",category:"answers"},{token:"Claude-SearchBot",provider:"Anthropic",category:"indexing"},{token:"ClaudeBot",provider:"Anthropic",category:"training"},{token:"Perplexity-User",provider:"Perplexity",category:"answers"},{token:"PerplexityBot",provider:"Perplexity",category:"indexing"}];function p(n){return l.find(r=>new RegExp(`(?:^|[\\s;(])${r.token}(?:/|[\\s;)]|$)`,"i").test(n))}async function h(n,r){let c=n.headers.get("user-agent")??"";if(!["GET","HEAD"].includes(n.method)||!p(c))return{status:"ignored"};let o=new URL(n.url);if(/\.(?:js|css|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|mp4|map)$/i.test(o.pathname)||/^\/(?:api|_next)(?:\/|$)/.test(o.pathname))return{status:"ignored"};let a=crypto.randomUUID(),e;try{if(e=new URL(r.collectorUrl??"https://collect.tracwell.app/v1/server/crawlers"),e.username||e.password||e.search||e.hash||e.protocol!=="https:"&&!(e.protocol==="http:"&&["localhost","127.0.0.1","[::1]"].includes(e.hostname))||!/^tw_sk_[A-Za-z0-9_-]+$/.test(r.serverKey))throw new Error("Invalid configuration")}catch{return{status:"failed",eventId:a,code:"CONFIGURATION"}}o.search="",o.hash="",o.username="",o.password="";let d=JSON.stringify({schema_version:1,event_id:a,timestamp:new Date().toISOString(),url:o.href,user_agent:c.slice(0,1024),...r.statusCode===void 0?{}:{status_code:r.statusCode}});for(let i=0;i<3;i++){try{let s=await fetch(e,{method:"POST",redirect:"error",signal:AbortSignal.timeout(2e3),headers:{"content-type":"application/json",authorization:`Bearer ${r.serverKey}`},body:d});if(s.status===202){let t=await s.json();return t&&typeof t=="object"&&"batch_id"in t&&t.batch_id===a&&"accepted_events"in t&&t.accepted_events===1&&"receipt_version"in t&&t.receipt_version===1?{status:"accepted",eventId:a}:{status:"failed",eventId:a,code:"DELIVERY_REJECTED"}}if(s.status!==408&&s.status!==429&&s.status<500)return{status:"failed",eventId:a,code:"DELIVERY_REJECTED"}}catch{}i<2&&await new Promise(s=>setTimeout(s,150*(i+1)))}return{status:"failed",eventId:a,code:"DELIVERY_UNAVAILABLE"}}export{h as trackAiCrawler};
@@ -25,6 +25,11 @@ export interface DeliveryReport {
25
25
  status: "accepted" | "handed_off";
26
26
  receiptId?: string;
27
27
  }
28
+ export interface TracwellSession {
29
+ anonymousId: string;
30
+ sessionId: string;
31
+ userId?: string;
32
+ }
28
33
  export interface TracwellConfig {
29
34
  projectKey: string;
30
35
  collectionMode?: CollectionMode;
@@ -42,6 +47,7 @@ export interface TracwellConfig {
42
47
  }
43
48
  export interface TracwellClient {
44
49
  flush(): Promise<void>;
50
+ getSession(): TracwellSession | undefined;
45
51
  identify(userId: string, properties?: EventProperties): string | undefined;
46
52
  page(properties?: EventProperties): string | undefined;
47
53
  reset(): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tracwell",
3
- "version": "0.1.0",
4
- "description": "Small, privacy-aware browser analytics SDK for Tracwell",
3
+ "version": "0.3.0",
4
+ "description": "Privacy-aware browser analytics and optional server-side AI crawler tracking for Tracwell",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": false,
@@ -13,6 +13,11 @@
13
13
  "types": "./dist/npm/index.d.ts",
14
14
  "import": "./dist/npm/index.js",
15
15
  "default": "./dist/npm/index.js"
16
+ },
17
+ "./server": {
18
+ "types": "./dist/npm/server.d.ts",
19
+ "import": "./dist/npm/server.js",
20
+ "default": "./dist/npm/server.js"
16
21
  }
17
22
  },
18
23
  "files": [
@@ -21,7 +26,9 @@
21
26
  "dist/npm/config.d.ts",
22
27
  "dist/npm/types.d.ts",
23
28
  "LICENSE",
24
- "README.md"
29
+ "README.md",
30
+ "dist/npm/server.js",
31
+ "dist/npm/server.d.ts"
25
32
  ],
26
33
  "keywords": [
27
34
  "analytics",