tracwell 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # SDK
2
2
 
3
3
  Browser analytics SDK for page views, custom events, identity, batching, and
4
- bounded delivery retries.
4
+ delivery retries that outlast short Collector outages.
5
5
 
6
6
  ## Script installation
7
7
 
@@ -48,9 +48,8 @@ window.tracwell?.track("signup_completed", { plan: "starter" });
48
48
 
49
49
  The script derives `/v1/events` from its own origin, so the same artifact works
50
50
  against the loopback Collector during local development. The production bundle
51
- reports SDK version `0.2.0`, targets ES2022-capable browsers, and has a 4.7 KB
52
- gzip budget enforced by
53
- `build:cdn`.
51
+ reports SDK version `0.2.0`, targets ES2022-capable browsers, and has the
52
+ accepted 5 KB (5,120-byte) gzip budget enforced by `build:cdn`.
54
53
 
55
54
  ## Public API
56
55
 
@@ -103,7 +102,7 @@ batching, retries, consent, and delivery remain in this core package.
103
102
  context.
104
103
  - Expose typed custom-event and identity APIs.
105
104
  - Batch only against the limits exported by `@tracwell/contracts`.
106
- - Retry bounded transient failures without changing event IDs.
105
+ - Retry transient failures with backoff without changing event IDs.
107
106
  - Respect configured consent and Do Not Track behavior before collection.
108
107
 
109
108
  The default SDK entry must remain browser-only, small, non-blocking, and unable
@@ -163,8 +162,19 @@ through `onError` and uses in-memory identity for the page lifetime.
163
162
  the shared 48 KiB limit.
164
163
  - Normal delivery reports `accepted` only after a matching `202` receipt
165
164
  confirms the batch ID and event count.
166
- - Network errors, `408`, `429`, and `5xx` responses use bounded in-memory retry
167
- without changing event or batch IDs.
165
+ - Network errors, `408`, `429`, and `5xx` responses are retried with backoff
166
+ (2 s doubling, at most 5 minutes per wait) without changing event or batch
167
+ IDs, until the oldest event nears the Collector's 24-hour age limit. An
168
+ explicit `maxRetries` (0-5) caps attempts; by default there is no fixed cap.
169
+ Periodic flushes wait for a scheduled retry; `flush()` and `shutdown()` send
170
+ immediately.
171
+ - While delivery is failing at most 100 events are held; the oldest waiting
172
+ events are dropped and reported through `onError`.
173
+ - Product mode with `persistence: "localStorage"` also stores held events under
174
+ `tracwell:<projectKey>:pending` and resumes them on the next page load. Private
175
+ mode never writes them to browser storage. Denying consent clears them.
176
+ Resending an event a beacon already delivered is safe: event IDs are stable
177
+ and deduplicated.
168
178
  - Other `4xx` responses report `DELIVERY_REJECTED` and are not retried.
169
179
  - Page-hide and hidden-document delivery prefers `sendBeacon`; a successful
170
180
  browser handoff reports `handed_off`, never `accepted`.
@@ -202,10 +212,21 @@ src/
202
212
  types.ts Public configuration, client, delivery, and error types
203
213
  client.test.ts Deterministic SDK behavior and failure tests
204
214
  index.test.ts SSR import and browser-initialization boundary
215
+ cli/ The `tracwell` command (Node only, own tsconfig.cli.json)
216
+ main.ts Commands, help and output
217
+ args.ts Flag parsing
218
+ config.ts Saved login and default website (0600 file)
219
+ oauth.ts Browser login, refresh and revoke
220
+ mcp.ts JSON-RPC client for `/mcp`
221
+ tool-arguments.ts Flags to tool arguments, --site/--days/requestId
222
+ install.ts Skill and AGENTS.md install into the current repo
223
+ definitions.ts `goals add` / `funnels add` step parsing
224
+ skills/tracwell/
225
+ SKILL.md Agent skill, shipped in the package and served at /skill.md
205
226
  scripts/
206
227
  build-cdn.mjs Minified browser build and gzip-size enforcement
207
- build-npm.mjs Self-contained ESM package build
208
- smoke-npm.mjs Packed runtime and TypeScript consumer smoke test
228
+ build-npm.mjs Self-contained ESM package build, including dist/npm/cli.js
229
+ smoke-npm.mjs Packed runtime, TypeScript, CLI and skill consumer smoke test
209
230
  static/
210
231
  _headers Cross-origin browser asset headers
211
232
  ```
@@ -263,3 +284,55 @@ handling because Workers does not support the fetch redirect error mode. Redirec
263
284
  responses are rejected without forwarding the server key.
264
285
 
265
286
  Version 0.4.0 expands server crawler recognition to 23 tokens across 12 providers. Existing server integrations must upgrade and redeploy to send newly supported crawlers; keys and browser installation are unchanged. The collector determines the purpose, including other for mixed/general crawling.
287
+
288
+ ## CLI
289
+
290
+ The package also installs a `tracwell` command for terminals and coding agents:
291
+
292
+ ```sh
293
+ npx tracwell init --url https://acme.com # sign in, find or add the site, snippet, skill
294
+ npx tracwell login # browser sign-in; pick websites on the consent screen
295
+ npx tracwell sites add Acme https://acme.com
296
+ npx tracwell use <website-id>
297
+ npx tracwell snippet # install snippet for the website
298
+ npx tracwell verify # waiting / receiving
299
+ npx tracwell overview --days 30
300
+ npx tracwell goals add "Signed up" --event signup_completed --category signup
301
+ npx tracwell funnels add Checkout --steps /pricing,event:checkout_started
302
+ npx tracwell search funnel # find tools by keyword
303
+ npx tracwell call funnel_create --name Checkout --dry-run
304
+ npx tracwell help overview # arguments, read live from Tracwell
305
+ npx tracwell skill install # .agents/skills/tracwell (and .claude/skills if present)
306
+ npx tracwell agents-md install
307
+ ```
308
+
309
+ `init` signs in if there is no saved login, then picks the website: `--site`,
310
+ else the one whose origin matches `--url` (it reads each website's install
311
+ record, since the list has no URLs), else creates it with `website_create`
312
+ (OAuth with "Add websites" only), else the saved or only website. It saves the
313
+ website as the default, prints the snippet, and runs `skill install` and
314
+ `agents-md install`. `skill install` leaves a copy that differs alone unless
315
+ `--force`; `agents-md install` replaces only the marked `tracwell:start/end`
316
+ block. `--dry-run` prints the tool, its arguments and whether it writes, without
317
+ calling it; it still reads the tool list, so it needs a login.
318
+
319
+ It is a thin client for the existing `/mcp` endpoint: each command is one
320
+ stateless `tools/call`, so the CLI has the same tools, permissions and plan
321
+ checks as MCP and needs no API of its own. Short commands cover the common
322
+ tools; `tracwell call <tool> --arg value` runs any other, and `tracwell tools`
323
+ lists what the current login can use. Output is JSON (`--compact` for one
324
+ line). Exit codes: 0 ok, 1 request failed, 2 bad input, 3 sign-in needed.
325
+
326
+ `login` registers a public OAuth client for a `http://127.0.0.1:<port>/callback`
327
+ redirect, opens the consent page with S256 PKCE and the `/mcp` resource, and
328
+ stores the access and refresh tokens. `login --key`, `--key` on any command, or
329
+ `TRACWELL_API_KEY` use an API key instead; keys cannot add websites. Credentials
330
+ live in `$TRACWELL_CONFIG_DIR`, else `$XDG_CONFIG_HOME/tracwell` or
331
+ `~/.config/tracwell` (`%APPDATA%\tracwell` on Windows), mode 0600. `TRACWELL_URL`
332
+ points the CLI at another Tracwell origin. `logout` revokes the OAuth grant and
333
+ deletes the file.
334
+
335
+ The CLI is built as a separate Node 20 entry (`dist/npm/cli.js`); the browser
336
+ and `tracwell/server` entries never import it. `skills/tracwell/SKILL.md` is the
337
+ agent skill: sign in, add the site, install the snippet, verify, read reports.
338
+ The web build serves the same file at `https://tracwell.app/skill.md`.
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import{randomUUID as ge}from"node:crypto";var S="0.6.0";function M(e){let t=[],r={};for(let i=0;i<e.length;i+=1){let a=e[i];if(a==="--"){t.push(...e.slice(i+1));break}if(a==="-h"){r.help=!0;continue}if(!a.startsWith("--")){t.push(a);continue}let c=a.indexOf("=");if(c!==-1){r[a.slice(2,c)]=a.slice(c+1);continue}let l=e[i+1];l!==void 0&&!l.startsWith("--")?(r[a.slice(2)]=l,i+=1):r[a.slice(2)]=!0}let[n,...s]=t;return{command:n,positional:s,flags:r}}function q(e){return e.replace(/-([a-z0-9])/gu,(t,r)=>r.toUpperCase())}import{chmod as he,mkdir as ye,readFile as ke,rm as be,writeFile as ve}from"node:fs/promises";import{homedir as Se}from"node:os";import{join as _}from"node:path";var xe="https://tracwell.app";function W(e=process.env){return e.TRACWELL_CONFIG_DIR?e.TRACWELL_CONFIG_DIR:process.platform==="win32"&&e.APPDATA?_(e.APPDATA,"tracwell"):_(e.XDG_CONFIG_HOME||_(Se(),".config"),"tracwell")}function I(){return _(W(),"config.json")}async function m(){try{return JSON.parse(await ke(I(),"utf8"))}catch(e){if(e.code==="ENOENT")return{};throw new Error(`Could not read ${I()}. Delete it and log in again.`,{cause:e})}}async function y(e){await ye(W(),{recursive:!0,mode:448});let t=I();await ve(t,`${JSON.stringify(e,null,2)}
3
+ `,{mode:384}),await he(t,384)}async function K(){await be(I(),{force:!0})}function P(e,t){return(e.TRACWELL_URL||t.login?.baseUrl||xe).replace(/\/+$/u,"")}var o=class extends Error{constructor(r,n=1){super(r);this.exitCode=n}exitCode};function B(e,t){try{return JSON.parse(e).error?.message??t}catch{return t}}async function H(e,t,r,n){let s;try{s=await fetch(`${e}/mcp`,{method:"POST",headers:{accept:"application/json, text/event-stream",authorization:`Bearer ${t}`,"content-type":"application/json","user-agent":`tracwell-cli/${S}`},body:JSON.stringify({jsonrpc:"2.0",id:1,method:r,params:n})})}catch{throw new o(`Could not reach ${e}. Check your connection and try again.`)}let i=await s.text();if(s.status===401)throw new o(`${B(i,"Not signed in.")} Run \`tracwell login\`.`,3);if(!s.ok)throw new o(B(i,`Tracwell returned HTTP ${s.status}.`));let a=i.split(`
4
+ `).find(l=>l.startsWith("data: "))?.slice(6)??i,c=JSON.parse(a);if(c.error)throw new o(c.error.message??"The request failed.");return c.result}var Te=e=>`${e.name.replaceAll("_"," ")} ${e.title??""} ${e.description??""}`.toLowerCase();function G(e,t){let r=t.toLowerCase().split(/\s+/u).filter(Boolean);if(!r.length)return[...e];let n=s=>r.filter(i=>s.name.toLowerCase().includes(i)).length;return e.filter(s=>r.every(i=>Te(s).includes(i))).toSorted((s,i)=>n(i)-n(s))}async function k(e,t){return(await H(e,t,"tools/list",{})).tools??[]}async function Y(e,t,r,n){return await H(e,t,"tools/call",{name:r,arguments:n})}function _e(e){let t=e.trim();if(!t)throw new o("A step is empty. Use a page like /pricing or an event like event:signup_completed.",2);if(t.startsWith("/"))return{type:"pageview",path:t};let r=t.startsWith("event:")?t.slice(6).trim():t;if(!r)throw new o("`event:` needs an event name, like event:signup_completed.",2);return{type:"event",name:r}}function z(e){let t=typeof e.page=="string"?e.page:void 0,r=typeof e.event=="string"?e.event:void 0;if(t&&r)throw new o("Pass --page or --event, not both.",2);if(t){if(!t.startsWith("/"))throw new o("--page must start with /, like /pricing.",2);return{type:"pageview",path:t}}if(r)return{type:"event",name:r};throw new o("Usage: tracwell goals add <name> --page /pricing (or --event signup_completed)",2)}function Z(e){if(typeof e!="string"||!e.trim())throw new o("Usage: tracwell funnels add <name> --steps /,/pricing,event:checkout_started",2);if(e.trim().startsWith("["))try{return JSON.parse(e)}catch{throw new o("--steps must be a comma list or a JSON array.",2)}let t=e.split(",").map(_e);if(t.length<2)throw new o("A funnel needs at least 2 steps.",2);return t}function L(e,t){return Object.fromEntries(Object.entries(e).filter(([r])=>!t.includes(r)))}import{access as Ie,mkdir as V,readFile as C,writeFile as X}from"node:fs/promises";import{dirname as Q,join as $,relative as Pe}from"node:path";var $e=new URL("../../skills/tracwell/SKILL.md",import.meta.url);function U(){return C($e,"utf8")}var Re=e=>Ie(e).then(()=>!0,()=>!1);async function Ae(e,t,r){let n;try{n=await C(e,"utf8")}catch{}return n===t?"unchanged":n!==void 0&&!r?"skipped":(await V(Q(e),{recursive:!0}),await X(e,t),n===void 0?"created":"updated")}async function N(e,t){let r=await U(),n=[$(e,".agents/skills/tracwell/SKILL.md")];return await Re($(e,".claude"))&&n.push($(e,".claude/skills/tracwell/SKILL.md")),Promise.all(n.map(async s=>({path:Pe(e,s),status:await Ae(s,r,t)})))}var ee="<!-- tracwell:start -->",O="<!-- tracwell:end -->",j=`${ee}
5
+ ## Analytics (Tracwell)
6
+
7
+ This project uses Tracwell for analytics. Use the \`tracwell\` CLI (\`npx tracwell\`)
8
+ to check the install and read reports: \`npx tracwell verify\`, \`npx tracwell overview\`,
9
+ and \`npx tracwell help\` for everything else. Before analytics work, read
10
+ \`.agents/skills/tracwell/SKILL.md\` (or run \`npx tracwell skill\`).
11
+ ${O}`;function Le(e){let t=e.indexOf(ee),r=e.indexOf(O,t);return t!==-1&&r!==-1?`${e.slice(0,t)}${j}${e.slice(r+O.length)}`:e.trim()?`${e.replace(/\s*$/u,"")}
12
+
13
+ ${j}
14
+ `:`${j}
15
+ `}async function E(e,t="AGENTS.md"){let r=$(e,t),n="";try{n=await C(r,"utf8")}catch{}let s=Le(n);return s===n?{path:t,status:"unchanged"}:(await V(Q(r),{recursive:!0}),await X(r,s),{path:t,status:n?"updated":"created"})}import{spawn as je}from"node:child_process";import{createHash as Oe,randomBytes as te}from"node:crypto";import{createServer as Ce}from"node:http";var ne="analytics:read definitions:write websites:write offline_access",Ue=300*1e3,Ne=60*1e3,F=e=>e.toString("base64url");async function re(e,t){let r=await fetch(e,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams(t).toString()}),n=await r.text();if(!r.ok){let s=`Sign-in failed (HTTP ${r.status}).`;try{let i=JSON.parse(n);s=i.error_description??i.error??s}catch{}throw new o(`${s} Run \`tracwell login\` again.`,3)}return JSON.parse(n)}function se(e,t,r,n){let s=r.refresh_token??n;return{type:"oauth",baseUrl:e,clientId:t,accessToken:r.access_token,...s?{refreshToken:s}:{},expiresAt:Date.now()+(r.expires_in??3600)*1e3}}function Ee(e){let[t,r]=process.platform==="darwin"?["open",[e]]:process.platform==="win32"?["rundll32",["url.dll,FileProtocolHandler",e]]:["xdg-open",[e]];try{let n=je(t,r,{detached:!0,stdio:"ignore"});n.on("error",()=>{}),n.unref()}catch{}}function Fe(){return new Promise((e,t)=>{let r=Ce();r.once("error",t),r.listen(0,"127.0.0.1",()=>{let n=r.address();n&&typeof n=="object"?e({server:r,port:n.port}):t(new o("Could not start the local sign-in callback."))})})}var De='<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>',Je='<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M12 7.5v5.5M12 16.5v.01"/></svg>',Me=(e,t,r=!1)=>`<!doctype html>
16
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${e} \xB7 Tracwell</title>
17
+ <style>
18
+ :root{--bg:#fff;--fg:#111;--muted:#737373;--shell:#f5f5f5;--card:#fff;--line:#e5e5e5;--chip:#f0f0f0;--blue:#357dff}
19
+ @media (prefers-color-scheme:dark){:root{--bg:#0a0a0a;--fg:#fafafa;--muted:#a3a3a3;--shell:#171717;--card:#0f0f0f;--line:#262626;--chip:#262626}}
20
+ *{box-sizing:border-box}html,body{margin:0;height:100%}
21
+ body{background:var(--bg);color:var(--fg);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;display:grid;place-items:center;padding:24px}
22
+ .shell{width:100%;max-width:360px;background:var(--shell);border-radius:24px;padding:6px}
23
+ .card{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:28px 24px;text-align:center}
24
+ .icon{width:44px;height:44px;margin:0 auto;border-radius:999px;display:grid;place-items:center;background:var(--chip);color:var(--muted)}
25
+ .ok .icon{background:var(--blue);color:#fff}
26
+ h1{margin:16px 0 0;font-size:16px;font-weight:600;letter-spacing:-.01em}
27
+ p{margin:6px 0 0;color:var(--muted)}
28
+ .foot{padding:10px 0 6px;text-align:center;font-size:12px;color:var(--muted)}
29
+ code{font:12px ui-monospace,SFMono-Regular,Menlo,monospace;background:var(--chip);padding:2px 6px;border-radius:6px;color:var(--fg);white-space:nowrap}
30
+ @media (prefers-reduced-motion:no-preference){.card{animation:in .3s cubic-bezier(.16,1,.3,1) both}@keyframes in{from{opacity:0;transform:translateY(4px)}}}
31
+ </style></head>
32
+ <body><main class="shell${r?" ok":""}"><div class="card"><div class="icon">${r?De:Je}</div><h1>${e}</h1><p>${t}</p></div>
33
+ <p class="foot">Tracwell CLI</p></main></body></html>`;function qe(e,t){return new Promise((r,n)=>{let s=setTimeout(()=>n(new o("Sign-in timed out. Run `tracwell login` again.",3)),Ue);e.on("request",(i,a)=>{let c=new URL(i.url??"/","http://127.0.0.1");if(c.pathname!=="/callback"){a.writeHead(404).end();return}let l=c.searchParams.get("error"),d=c.searchParams.get("code"),p=(T,v,h,w=!1)=>a.writeHead(T,{"content-type":"text/html; charset=utf-8"}).end(Me(v,h,w));if(c.searchParams.get("state")!==t){p(400,"This sign-in link is out of date","Go back to your terminal and run <code>tracwell login</code> again.");return}if(clearTimeout(s),l||!d){p(400,"Sign-in was cancelled","Nothing was connected. Run <code>tracwell login</code> to try again."),n(new o(c.searchParams.get("error_description")??"Sign-in was cancelled.",3));return}p(200,"You're signed in","The Tracwell CLI is connected. You can close this tab and go back to your terminal.",!0),r(d)})})}async function ie(e,t){let{server:r,port:n}=await Fe();try{let s=`http://127.0.0.1:${n}/callback`,i=await fetch(`${e}/api/auth/oauth2/register`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({client_name:"Tracwell CLI",redirect_uris:[s],token_endpoint_auth_method:"none",grant_types:["authorization_code","refresh_token"],response_types:["code"],scope:ne})}).catch(()=>{throw new o(`Could not reach ${e}. Check your connection and try again.`)});if(!i.ok)throw new o(`Could not start sign-in (HTTP ${i.status}).`,3);let{client_id:a}=await i.json(),c=F(te(32)),l=F(te(16)),d=new URL("/api/auth/oauth2/authorize",e);for(let[v,h]of Object.entries({client_id:a,redirect_uri:s,response_type:"code",scope:ne,state:l,code_challenge:F(Oe("sha256").update(c).digest()),code_challenge_method:"S256",resource:`${e}/mcp`}))d.searchParams.set(v,h);t.print("Sign in and pick your websites in the browser:"),t.print(d.href),t.open&&Ee(d.href);let p=await qe(r,l),T=await re(`${e}/api/auth/oauth2/token`,{grant_type:"authorization_code",code:p,client_id:a,redirect_uri:s,code_verifier:c,resource:`${e}/mcp`});return se(e,a,T)}finally{r.close()}}function oe(e,t=Date.now()){return e.expiresAt-Ne<=t}async function ae(e){if(!e.refreshToken)throw new o("Your sign-in expired. Run `tracwell login`.",3);let t=await re(`${e.baseUrl}/api/auth/oauth2/token`,{grant_type:"refresh_token",refresh_token:e.refreshToken,client_id:e.clientId,resource:`${e.baseUrl}/mcp`});return se(e.baseUrl,e.clientId,t,e.refreshToken)}async function ce(e){let t=e.refreshToken??e.accessToken;await fetch(`${e.baseUrl}/api/auth/oauth2/revoke`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({token:t,client_id:e.clientId}).toString()}).catch(()=>{})}var We=new Set(["json","site","days","compact","key","help","no-browser","dry-run","force"]),R=7,le=e=>e.toISOString().slice(0,10);function Ke(e,t){let r=new Date(Date.UTC(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate()-(e-1)));return{from:le(r),to:le(t)}}function Be(e,t,r,n,s){let i="from"in e&&"to"in e,a=!i&&"date"in e;if(!i&&!a)return{};if(r.from!==void 0||r.to!==void 0||r.date!==void 0)return{};if(!(t.has("from")||t.has("date")||n.days!==void 0))return{};let c=n.days===void 0?R:Number(n.days);if(!Number.isInteger(c)||c<1)throw new o("--days must be a whole number of 1 or more.",2);let{from:l,to:d}=Ke(c,s);if(a)return{date:l,..."to"in e?{to:d}:{}};if(e.from?.format==="date-time"){let p=new Date(`${d}T00:00:00.000Z`);return p.setUTCDate(p.getUTCDate()+1),{from:`${l}T00:00:00.000Z`,to:p.toISOString()}}return{from:l,to:d}}function He(e){return e?.type?Array.isArray(e.type)?e.type:[e.type]:[]}function Ge(e,t,r){let n=He(r);if(t===!0){if(n.length&&!n.includes("boolean"))throw new o(`--${e} needs a value.`,2);return!0}if(n.includes("number")||n.includes("integer")){let s=Number(t);if(Number.isNaN(s))throw new o(`--${e} must be a number.`,2);return s}if(n.includes("boolean"))return t==="true";if(!n.length&&/^\s*[[{]/u.test(t))try{return JSON.parse(t)}catch{throw new o(`--${e} must be JSON.`,2)}if(n.includes("object")||n.includes("array"))try{return JSON.parse(t)}catch{throw new o(`--${e} must be JSON.`,2)}return t}function de(e,t,r){let n=e.inputSchema?.properties??{},s=new Set(e.inputSchema?.required??[]),i={};if(typeof t.json=="string")try{i=JSON.parse(t.json)}catch{throw new o("--json must be a JSON object.",2)}for(let[c,l]of Object.entries(t)){if(We.has(c))continue;let d=q(c);i[d]=Ge(c,l,n[d])}let a=typeof t.site=="string"?t.site:r.site;return"projectId"in n&&i.projectId===void 0&&a&&(i.projectId=a),Object.assign(i,Be(n,s,i,t,r.today)),"requestId"in n&&i.requestId===void 0&&(i.requestId=r.requestId()),i}var J={sites:{tool:"analytics_projects",summary:"List the websites you can read"},snippet:{tool:"install_snippet",summary:"Get the install snippet for a website"},verify:{tool:"install_verify",summary:"Check whether a website is receiving events"},overview:{tool:"analytics_overview",summary:"Traffic overview"},acquisition:{tool:"analytics_acquisition",summary:"Sources, referrers and campaigns"},revenue:{tool:"analytics_revenue",summary:"Revenue and where it came from"},events:{tool:"analytics_events",summary:"Event names and counts"},journeys:{tool:"analytics_journeys",summary:"Recorded sessions"},goals:{tool:"analytics_goals",summary:"Saved goals"},goal:{tool:"analytics_goal",summary:"One goal's report (--goal-id)"},funnels:{tool:"analytics_funnels",summary:"Saved funnels"},funnel:{tool:"analytics_funnel",summary:"One funnel's report (--funnel-id)"},notes:{tool:"notes_list",summary:"Chart notes"}},ue=`tracwell ${S} - Tracwell analytics from your terminal or coding agent
34
+
35
+ Start here
36
+ tracwell init [--url <url>] [--name <name>]
37
+ Sign in, pick or add the website, print the snippet,
38
+ and install the agent skill in this repo
39
+
40
+ Setup
41
+ tracwell login Sign in in the browser and pick websites
42
+ tracwell login --key <key> Use an API key from Settings \u2192 API & MCP instead
43
+ tracwell sites add <name> <url> Add a website (browser login only)
44
+ tracwell use <website-id> Default website for later commands
45
+ tracwell snippet Install snippet for the default website
46
+ tracwell verify Check that events are arriving
47
+ tracwell whoami Who you are signed in as and which websites
48
+ tracwell logout
49
+
50
+ Reports (default: last ${R} days, UTC)
51
+ ${Object.entries(J).filter(([e])=>!["sites","snippet","verify"].includes(e)).map(([e,{summary:t}])=>` tracwell ${e.padEnd(22)} ${t}`).join(`
52
+ `)}
53
+
54
+ Goals and funnels (need "Manage goals and funnels")
55
+ tracwell goals add <name> --page /pricing or --event signup_completed
56
+ [--category signup|revenue|engagement|other]
57
+ tracwell funnels add <name> --steps /,/pricing,event:checkout_started
58
+ [--window-hours 24]
59
+
60
+ Any tool
61
+ tracwell search <words> Find tools by keyword
62
+ tracwell tools List every tool this login can use
63
+ tracwell call <tool> [--arg value ...] [--json '{...}']
64
+ tracwell help <command|tool> Arguments for a command, read live from Tracwell
65
+
66
+ Agent setup
67
+ tracwell skill Print the agent skill
68
+ tracwell skill install [--force] Write it to .agents/skills (and .claude/skills)
69
+ tracwell agents-md install [--file AGENTS.md] Add a Tracwell block to AGENTS.md
70
+
71
+ Common flags
72
+ --site <website-id> Website for this command
73
+ --days <n> Report the last n days; or pass --from and --to (YYYY-MM-DD)
74
+ --dry-run Print the tool and arguments without sending anything
75
+ --compact One-line JSON output
76
+ --key <key> API key for this command (or set TRACWELL_API_KEY)
77
+
78
+ Output is JSON. Exit codes: 0 ok, 1 request failed, 2 bad input, 3 sign in needed.`,b=e=>{process.stdout.write(`${e}
79
+ `)},A=e=>{process.stderr.write(`${e}
80
+ `)};function f(e,t){b(typeof e=="string"?e:JSON.stringify(e,null,t.compact?void 0:2))}function Ye(e){if(e.structuredContent!==void 0)return e.structuredContent;let t=(e.content??[]).filter(r=>r.type==="text").map(r=>r.text??"").join(`
81
+ `);try{return JSON.parse(t)}catch{return t}}async function u(e){let t=await m(),r=P(process.env,t),n=typeof e.key=="string"?e.key:process.env.TRACWELL_API_KEY;if(n)return{baseUrl:r,token:n,settings:t,viaKey:!0};let s=t.login;if(!s||s.baseUrl!==r)throw new o("Not signed in. Run `tracwell login`, or set TRACWELL_API_KEY.",3);if(s.type==="key")return{baseUrl:r,token:s.key,settings:t,viaKey:!0};if(!oe(s))return{baseUrl:r,token:s.accessToken,settings:t,viaKey:!1};let i=await ae(s),a={...t,login:i};return await y(a),{baseUrl:r,token:i.accessToken,settings:a,viaKey:!1}}async function pe(e){let t=await m(),r=await ie(P(process.env,t),{open:!e["no-browser"],print:A});await y({...t,login:r})}async function fe(e,t){let r=(await k(e.baseUrl,e.token)).find(n=>n.name===t);if(!r)throw new o(`This login cannot use \`${t}\`. Run \`tracwell tools\` to see what it can use.`,2);return r}async function x(e,t,r){let n=await Y(e.baseUrl,e.token,t,r),s=Ye(n);if(n.isError)throw new o(typeof s=="string"?s:JSON.stringify(s));return s}async function g(e,t,r,n={}){let s=await fe(e,t),i={...de(s,r,{...e.settings.site?{site:e.settings.site}:{},today:new Date,requestId:ge}),...n};if(r["dry-run"])return f({dryRun:!0,tool:t,writes:s.annotations?.readOnlyHint===!1,arguments:i},r);f(await x(e,t,i),r)}function ze(e,t){let r=e.inputSchema?.properties??{},n=new Set(e.inputSchema?.required??[]),s=Object.entries(r).map(([i,a])=>{let c=Array.isArray(a.type)?a.type.join("|"):a.type??"value",l=i==="projectId"?" (or --site, or `tracwell use`)":i==="requestId"?" (filled in for you)":i==="from"||i==="to"||i==="date"?` (or --days; default last ${R} days)`:"";return` --${i.padEnd(18)} ${c}${n.has(i)?", required":""}${l}${a.description?`
82
+ ${a.description}`:""}`});return[`tracwell ${t} (${e.name})`,"",e.description??"","","Arguments",...s].join(`
83
+ `)}async function Ze(e,t){let r=async l=>await x(e,"install_snippet",{projectId:l});if(typeof t.site=="string")return{installation:await r(t.site),created:!1};let{projects:n}=await x(e,"analytics_projects",{limit:100}),s=typeof t.url=="string"?t.url:void 0;if(s){let l;try{l=new URL(s).origin}catch{throw new o("--url must be a full address, like https://acme.com.",2)}let p=(await Promise.all(n.map(w=>r(w.id)))).find(w=>w.origins.some(we=>new URL(we).origin===l));if(p)return{installation:p,created:!1};if(!(await k(e.baseUrl,e.token)).some(w=>w.name==="website_create"))throw new o(e.viaKey?'API keys cannot add websites. Run `tracwell login` and tick "Add websites", or add it in the dashboard and pass --site.':'This login cannot add websites. Run `tracwell login` again and tick "Add websites".',3);let v=typeof t.name=="string"?t.name:new URL(s).hostname.replace(/^www\./u,""),h=await x(e,"website_create",{requestId:ge(),name:v,websiteUrl:s});return{installation:h.website,created:h.created}}let i=n.find(l=>l.id===e.settings.site),a=n.length===1?n[0]:void 0,c=i??a;if(c)return{installation:await r(c.id),created:!1};throw n.length?new o(`Several websites: ${n.map(l=>`${l.name} (${l.id})`).join(", ")}. Pass --site <id> or --url <url>.`,2):new o("No websites yet. Run `tracwell init --url https://your-site.com` to add one.",2)}function D(e){return e.map(t=>`${t.path}: ${t.status==="skipped"?"exists and differs; rerun with --force to replace":t.status}`)}async function Ve(e){let{command:t,positional:r,flags:n}=M(e);if(n.version||t==="version")return b(S);if(!t||t==="help"&&!r[0])return b(ue);if(t==="help"||n.help&&!["skill","agents-md","init","search"].includes(t)){let s=t==="help"?r:[t,...r],i=s[0],a={goals:"goal_create",funnels:"funnel_create",sites:"website_create"},c=(s[1]==="add"?a[i]:void 0)??J[i]?.tool??(i==="call"?s[1]:i);if(!c)return b(ue);let l=await u(n),d=s[1]==="add"&&a[i]?`${i} add`:i;return b(ze(await fe(l,c),d))}switch(t){case"login":{if(typeof n.key=="string"){let s=await m(),i=P(process.env,s);await k(i,n.key),await y({...s,login:{type:"key",baseUrl:i,key:n.key}})}else await pe(n);A("Signed in. Run `tracwell sites` to see your websites.");return}case"logout":{let s=await m();s.login?.type==="oauth"&&await ce(s.login),await K(),A("Signed out.");return}case"use":{let s=r[0];if(!s)throw new o("Usage: tracwell use <website-id>. Run `tracwell sites` for IDs.",2);let i=await m();await y({...i,site:s}),A(`Default website set to ${s}.`);return}case"whoami":{let s=await u(n);return f({baseUrl:s.baseUrl,login:s.viaKey?"api key":"browser",defaultSite:s.settings.site??null,websites:await x(s,"analytics_projects",{})},n)}case"tools":{let s=await u(n),i=await k(s.baseUrl,s.token);return f(i.map(a=>({name:a.name,description:a.description,writes:a.annotations?.readOnlyHint===!1})),n)}case"search":{let s=r.join(" ");if(!s)throw new o("Usage: tracwell search <words>",2);let i=await u(n),a=G(await k(i.baseUrl,i.token),s);return f(a.map(c=>({name:c.name,description:c.description,writes:c.annotations?.readOnlyHint===!1})),n)}case"call":{let s=r[0];if(!s)throw new o("Usage: tracwell call <tool> [--arg value ...]. Run `tracwell tools` for names.",2);return g(await u(n),s,n)}case"goals":{if(r[0]!=="add")return g(await u(n),"analytics_goals",n);let s=r[1];if(!s)throw new o("Usage: tracwell goals add <name> --page /pricing (or --event signup_completed)",2);let i=z(n);return g(await u(n),"goal_create",L(n,["page","event"]),{name:s,match:i,...n.category===void 0?{category:"other"}:{}})}case"funnels":{if(r[0]!=="add")return g(await u(n),"analytics_funnels",n);let s=r[1];if(!s)throw new o("Usage: tracwell funnels add <name> --steps /,/pricing,event:checkout_started",2);let i=Z(n.steps),a=n["window-hours"]!==void 0||n.windowHours!==void 0;return g(await u(n),"funnel_create",L(n,["steps"]),{name:s,steps:i,...a?{}:{windowHours:24}})}case"sites":{if(r[0]==="add"){let[,s,i]=r;if(!s||!i)throw new o("Usage: tracwell sites add <name> <url>",2);return g(await u(n),"website_create",n,{name:s,websiteUrl:i})}return g(await u(n),"analytics_projects",n)}case"skill":{if(!r[0])return b(await U());if(r[0]!=="install")throw new o("Usage: tracwell skill [install [--force]]",2);return f({installed:D(await N(process.cwd(),!!n.force))},n)}case"agents-md":{if(r[0]!=="install")throw new o("Usage: tracwell agents-md install [--file AGENTS.md]",2);let s=typeof n.file=="string"?n.file:"AGENTS.md";return f({installed:D([await E(process.cwd(),s)])},n)}case"init":{let s;try{s=await u(n)}catch(l){if(!(l instanceof o)||l.exitCode!==3)throw l;await pe(n),s=await u(n)}let{installation:i,created:a}=await Ze(s,n);await y({...await m(),site:i.projectId});let c=[...await N(process.cwd(),!!n.force),await E(process.cwd())];return f({website:{id:i.projectId,name:i.name,origins:i.origins,created:a},snippet:{script:i.script,npm:i.npm,placement:i.placement},installed:D(c),next:"Add the script tag (or the npm setup) to the site, load a page, then run `tracwell verify`."},n)}default:{let s=J[t];if(!s)throw new o(`Unknown command \`${t}\`. Run \`tracwell help\`.`,2);return g(await u(n),s.tool,n)}}}Ve(process.argv.slice(2)).catch(e=>{let t=e instanceof o;process.stderr.write(`tracwell: ${t||e instanceof Error?e.message:String(e)}
84
+ `),process.exitCode=t?e.exitCode:1});
@@ -2,7 +2,17 @@ import type { NormalizedConfig, SdkError, TracwellConfig } from "./types";
2
2
  export declare const DEFAULT_COLLECTOR_ENDPOINT = "https://collect.tracwell.app/v1/events";
3
3
  export declare const DEFAULT_BATCH_SIZE = 25;
4
4
  export declare const DEFAULT_FLUSH_INTERVAL_MS = 10000;
5
- export declare const DEFAULT_MAX_RETRIES = 2;
5
+ /**
6
+ * No fixed attempt cap: a failed batch is retried with backoff until its oldest
7
+ * event approaches the Collector's event-age limit. An explicit `maxRetries`
8
+ * (0-5) still caps attempts.
9
+ */
10
+ export declare const DEFAULT_MAX_RETRIES: number;
11
+ /** Retry backoff doubles from 2 s up to 5 min. The Collector sends no Retry-After. */
12
+ export declare const RETRY_BASE_DELAY_MS = 2000;
13
+ export declare const RETRY_MAX_DELAY_MS: number;
14
+ /** Events held while delivery is failing; the oldest waiting events are dropped beyond it. */
15
+ export declare const MAX_HELD_EVENTS = 100;
6
16
  export declare const DEFAULT_SESSION_TIMEOUT_MS: number;
7
17
  export declare const SDK_VERSION = "0.2.0";
8
18
  export declare function normalizeProjectKey(value: string, onError?: (error: SdkError) => void): string;
package/dist/npm/index.js CHANGED
@@ -1 +1 @@
1
- var k="https://collect.tracwell.app/v1/events",O=25,H=1e4,B=2,X=1800*1e3,b="0.2.0";function E(e,t){throw t?.({code:"CONFIG_INVALID",message:e}),new TypeError(e)}function et(e,t){let n=e.trim();return n.length<20||n.length>128||!/^[A-Za-z0-9_-]+$/u.test(n)?E("projectKey does not match the public project-key contract.",t):n}function nt(e,t){let n;try{n=new URL(e)}catch{return E("endpoint must be an absolute HTTPS URL.",t)}return rt(n,t)}function rt(e,t){let n=e.hostname==="localhost"||e.hostname==="127.0.0.1"||e.hostname==="[::1]";return e.protocol!=="https:"&&!n?E("endpoint must use HTTPS unless it uses a loopback host.",t):e.toString()}function y(e,t,n,r,i){return!Number.isSafeInteger(t)||t<n||t>r?E(`${e} must be an integer between ${n} and ${r}.`,i):t}function G(e){let t=et(e.projectKey,e.onError),n=e.collectionMode??"private",r=nt(e.endpoint??k,e.onError);return n==="private"&&e.persistence!==void 0&&e.persistence!=="none"?E('Private collection requires persistence: "none".',e.onError):{autoTrackPageViews:e.autoTrackPageViews??!0,batchSize:y("batchSize",e.batchSize??O,1,50,e.onError),consent:e.consent??"granted",collectionMode:n,endpoint:r,flushIntervalMs:y("flushIntervalMs",e.flushIntervalMs??H,1e3,6e4,e.onError),maxRetries:y("maxRetries",e.maxRetries??B,0,5,e.onError),persistence:n==="private"?"none":e.persistence??"localStorage",projectKey:t,respectDoNotTrack:e.respectDoNotTrack??!0,sessionTimeoutMs:y("sessionTimeoutMs",e.sessionTimeoutMs??X,6e4,1440*60*1e3,e.onError),...e.onDelivery?{onDelivery:e.onDelivery}:{},...e.onError?{onError:e.onError}:{}}}var Y=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"];function it(e){let t=e?.trim();return t||void 0}function ot(e){if(e)try{return new URL(e).toString()}catch{return}}function A(e){let t=new URL(e.href),n={},r=ot(e.referrer);r&&(n.referrer=r);for(let i of Y){let o=it(t.searchParams.get(i));o&&(n[i]=o)}return n}function R(e){let t=Y.map(n=>e[n]??"").join("");return t.replaceAll("","")?t:void 0}var w=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu,yt=/^[A-Za-z][A-Za-z0-9_.:-]*$/u,Tt=/[\s\x00-\x1f\x7f]/u,St=/[\x00-\x1f\x7f]/u;function C(e){return new TextEncoder().encode(JSON.stringify(e)).byteLength}function N(e){return e.length>0&&e.length<=256&&!Tt.test(e)}function K(e,t=2048){if(e.length>t)return!1;try{return new URL(e).origin.length>0}catch{return!1}}function L(e,t,n){return t>4?!1:typeof e=="string"?e.length<=1024:typeof e=="number"?Number.isFinite(e):e===null||typeof e=="boolean"?!0:Array.isArray(e)?e.length<=50&&e.every(r=>L(r,t+1,n)):Object.entries(e).every(([r,i])=>(n.count+=1,n.count<=100&&r.length>0&&r.length<=128&&!St.test(r)&&L(i,t+1,n)))}function z(e){let{context:t}=e;try{if(C(e)>16384)return!1}catch{return!1}return w.test(e.event_id)&&N(e.anonymous_id)&&N(e.session_id)&&(e.user_id===void 0||N(e.user_id))&&(e.event_type!=="event"||e.event_name.length<=100&&yt.test(e.event_name)&&e.event_name!=="identify"&&e.event_name!=="page_view")&&K(t.url)&&t.path.startsWith("/")&&t.path.length<=1024&&(t.title===void 0||t.title.length<=512)&&(t.referrer===void 0||K(t.referrer))&&[t.utm_source,t.utm_medium,t.utm_campaign,t.utm_term,t.utm_content].every(n=>n===void 0||n.length>0&&n.length<=256)&&(e.properties===void 0||L(e.properties,0,{count:0}))}function j(e){if(!e||typeof e!="object")return;let t=e;if(!(t.receipt_version!==1||typeof t.receipt_id!="string"||!w.test(t.receipt_id)||typeof t.batch_id!="string"||!w.test(t.batch_id)||!Number.isInteger(t.accepted_events)||(t.accepted_events??0)<1||(t.accepted_events??0)>50||typeof t.accepted_at!="string"||Number.isNaN(Date.parse(t.accepted_at))))return t}function It(e,t){let n=new URL(e.href);return n.hash="",{path:n.pathname,url:n.toString(),...e.title?{title:e.title}:{},...t}}function F(e,t,n,r){let i={schema_version:1,event_id:e.randomUUID(),event_name:n.eventName,event_type:n.eventType,timestamp:new Date(e.now()).toISOString(),anonymous_id:t.anonymousId,session_id:t.sessionId,context:It(e.page(),t.attribution),...n.properties?{properties:n.properties}:{},...n.userId?{user_id:n.userId}:{}};if(!z(i)){r?.({code:"EVENT_INVALID",message:"Event was not queued because it failed the v1 contract."});return}return i}var J=1;function gt(e){if(!e||typeof e!="object")return!1;let t=e;return t.version===J&&typeof t.anonymousId=="string"&&typeof t.sessionId=="string"&&typeof t.lastActivityAt=="number"&&typeof t.attribution=="object"&&t.attribution!==null&&(t.userId===void 0||typeof t.userId=="string")}var T=class{#e;#o;#t;#i;#r;#n;constructor(t,n){this.#t=t,this.#o=n.onError,this.#i=n.sessionTimeoutMs,this.#e=`tracwell:${n.projectKey}:identity`,this.#r=n.persistence==="localStorage"?this.#p(t):void 0}identify(t,n){let r=this.#d(n);return r.userId&&r.userId!==t&&(r.anonymousId=this.#t.randomUUID(),r.sessionId=this.#t.randomUUID()),r.userId=t,this.#h(r),this.#s(r)}reset(){if(this.#n=void 0,this.#r)try{this.#r.removeItem(this.#e)}catch(t){this.#a(t)}}snapshot(t){return this.#s(this.#d(t))}#c(t){let n=this.#t.now();return{anonymousId:this.#t.randomUUID(),attribution:t,lastActivityAt:n,sessionId:this.#t.randomUUID(),version:J}}#d(t){let n=this.#t.now(),r=this.#u();if(!r){let a=this.#c(t);return this.#h(a),a}let i=R(r.attribution),o=R(t);return(n-r.lastActivityAt>=this.#i||o!==void 0&&o!==i)&&(r.sessionId=this.#t.randomUUID(),r.attribution=t),r.lastActivityAt=n,this.#h(r),r}#u(){if(this.#n)return this.#n;if(this.#r)try{let t=this.#r.getItem(this.#e);if(!t)return;let n=JSON.parse(t);if(!gt(n)){this.#r.removeItem(this.#e);return}return this.#n=n,n}catch(t){this.#a(t);return}}#p(t){try{return t.storage()}catch(n){this.#a(n);return}}#a(t){this.#o?.({cause:t,code:"STORAGE_UNAVAILABLE",message:"Persistent identity storage is unavailable; using in-memory identity."})}#s(t){return{anonymousId:t.anonymousId,attribution:t.attribution,sessionId:t.sessionId,...t.userId?{userId:t.userId}:{}}}#h(t){if(this.#n=t,!!this.#r)try{this.#r.setItem(this.#e,JSON.stringify(t))}catch(n){this.#a(n)}}};function f(e,t,n,r){return{...r===void 0?{}:{cause:r},code:e,eventIds:n.batch.events.map(i=>i.event_id),message:t}}var S=class{#e;#o;#t;#i;#r;#n=[];constructor(t,n){this.#o=t,this.#e=n}enqueue(t){this.#n.push(t),this.#n.length>=this.#e.batchSize&&this.flush()}clear(){this.#i?.(),this.#i=void 0,this.#t=void 0,this.#n=[]}flush(){return this.#r?this.#r:(this.#r=this.#c().finally(()=>{this.#r=void 0}),this.#r)}flushWithBeacon(){for(this.#i?.(),this.#i=void 0;this.#t||this.#n.length>0;){let t=this.#t??this.#d();if(!t)return;let n=JSON.stringify(t.batch);if(!this.#o.sendBeacon(this.#e.endpoint,n)){this.#e.onError?.(f("DELIVERY_FAILED","Browser lifecycle handoff was rejected by sendBeacon.",t));return}this.#t=void 0,this.#u({batchId:t.batch.batch_id,eventIds:t.batch.events.map(r=>r.event_id),status:"handed_off"})}}async#c(){this.#i?.(),this.#i=void 0;let t=this.#t??this.#d();if(!t)return;await this.#a(t)&&this.#n.length>0&&await this.#c()}#d(){if(this.#n.length===0)return;let t=this.#n.slice(0,this.#e.batchSize),n;for(;t.length>0;){let r={batch_version:1,batch_id:this.#o.randomUUID(),project_key:this.#e.projectKey,sent_at:new Date(this.#o.now()).toISOString(),sdk:{name:"tracwell-browser",version:b},events:t};if(C(r)<=49152){n=r;break}t.pop()}if(!n){let r=this.#n.shift();r&&this.#e.onError?.({code:"EVENT_INVALID",eventIds:[r.event_id],message:"Event cannot fit in a valid browser batch."});return}return this.#n.splice(0,n.events.length),this.#t={attempt:0,batch:n},this.#t}#u(t){this.#e.onDelivery?.(t)}#p(t){let n=250*2**(t.attempt-1);this.#i=this.#o.setTimeout(()=>{this.#i=void 0,this.flush()},n)}async#a(t){let n;try{n=await this.#o.fetch(this.#e.endpoint,JSON.stringify(t.batch),"text/plain;charset=UTF-8")}catch(o){return this.#t!==t?!1:this.#s(t,o)}if(n.status!==202)return n.status===408||n.status===429||n.status>=500?this.#s(t,new Error(`Collector returned HTTP ${n.status}.`)):(this.#t===t&&(this.#t=void 0),this.#e.onError?.(f("DELIVERY_REJECTED",`Collector rejected the batch with non-retryable HTTP ${n.status}.`,t)),!1);let r;try{r=await n.json()}catch(o){return this.#e.onError?.(f("RECEIPT_INVALID","Collector returned an unreadable acceptance receipt.",t,o)),this.#s(t,o)}let i=j(r);return!i||i.batch_id!==t.batch.batch_id||i.accepted_events!==t.batch.events.length?(this.#e.onError?.(f("RECEIPT_INVALID","Collector receipt does not match the submitted batch.",t)),this.#s(t,new Error("Acceptance receipt mismatch."))):(this.#t===t&&(this.#t=void 0),this.#u({batchId:t.batch.batch_id,eventIds:t.batch.events.map(o=>o.event_id),receiptId:i.receipt_id,status:"accepted"}),!0)}#s(t,n){return this.#t!==t?!1:(t.attempt+=1,t.attempt<=this.#e.maxRetries?(this.#e.onError?.(f("DELIVERY_FAILED","Event batch delivery failed and is scheduled for bounded retry.",t,n)),this.#p(t),!1):(this.#t=void 0,this.#e.onError?.(f("DELIVERY_EXHAUSTED","Event batch delivery exhausted the configured retry limit.",t,n)),!1))}};function $(e,t){let n=new T(e,t),r=new S(e,t),i=t.consent,o,p=!1,c=!1,a=[];function _(){return i==="granted"&&!(t.respectDoNotTrack&&e.doNotTrackEnabled())}function I(s,u,d,h){if(c||!_())return;let V=A(e.page()),D=n.snapshot(V),l=h??D.userId,Q=s==="identify"&&l?{eventName:"identify",eventType:"identify",...d?{properties:d}:{},userId:l}:s==="pageview"?{eventName:"page_view",eventType:"pageview",...d?{properties:d}:{},...l?{userId:l}:{}}:{eventName:u,eventType:"event",...d?{properties:d}:{},...l?{userId:l}:{}},m=F(e,D,Q,t.onError);if(m){if(s==="identify"&&h){let U=n.identify(h,V);m.anonymous_id=U.anonymousId,m.session_id=U.sessionId}return r.enqueue(m),m.event_id}}function g(s){if(!_())return;let u=new URL(e.page().href);u.hash="";let d=u.toString();if(d===o)return;let h=I("pageview","page_view",s);return h&&(o=d),h}function x(){a.forEach(s=>s()),a=[],p=!1}function M(){p||c||!_()||(p=!0,a=[e.addPageHideListener(()=>r.flushWithBeacon()),e.addVisibilityHiddenListener(()=>r.flushWithBeacon()),e.setInterval(()=>{r.flush()},t.flushIntervalMs)],t.autoTrackPageViews&&(a.push(e.subscribeToNavigation(()=>{g()})),g()))}return M(),{flush(){return r.flush()},getSession(){if(c||t.collectionMode!=="product"||!_())return;let s=n.snapshot(A(e.page()));return{anonymousId:s.anonymousId,sessionId:s.sessionId,...s.userId?{userId:s.userId}:{}}},identify(s,u){if(t.collectionMode==="private"){t.onError?.({code:"IDENTIFY_UNAVAILABLE",message:"identify() is unavailable in Private collection mode."});return}return I("identify","identify",u,s)},page:g,reset(){n.reset(),o=void 0},setConsent(s){if(i=s,s==="denied"){x(),r.clear(),n.reset(),o=void 0;return}M()},async shutdown(){c||(x(),await r.flush(),c=!0)},track(s,u){return I("event",s,u)}}}function q(e,t){return window.addEventListener(e,t),()=>window.removeEventListener(e,t)}function W(){return{addPageHideListener(e){return q("pagehide",e)},addVisibilityHiddenListener(e){let t=()=>{document.visibilityState==="hidden"&&e()};return document.addEventListener("visibilitychange",t),()=>document.removeEventListener("visibilitychange",t)},doNotTrackEnabled(){return navigator.doNotTrack==="1"},async fetch(e,t,n){return fetch(e,{body:t,credentials:"omit",headers:{"content-type":n},keepalive:!0,method:"POST"})},now(){return Date.now()},page(){return{href:window.location.href,referrer:document.referrer,title:document.title}},randomUUID(){return crypto.randomUUID()},sendBeacon(e,t){return navigator.sendBeacon(e,t)},setInterval(e,t){let n=window.setInterval(e,t);return()=>window.clearInterval(n)},setTimeout(e,t){let n=window.setTimeout(e,t);return()=>window.clearTimeout(n)},storage(){return window.localStorage},subscribeToNavigation(e){let t=!0,n=history.pushState,r=history.replaceState,i=()=>{t&&queueMicrotask(e)},o=function(...a){n.apply(history,a),i()},p=function(...a){r.apply(history,a),i()};history.pushState=o,history.replaceState=p;let c=q("popstate",e);return()=>{t=!1,c(),history.pushState===o&&(history.pushState=n),history.replaceState===p&&(history.replaceState=r)}}}}function Jt(e){if(typeof window>"u"||typeof document>"u"||typeof navigator>"u")throw new Error("createTracwell must be called in a browser after the document is available.");return $(W(),G(e))}export{O as DEFAULT_BATCH_SIZE,k as DEFAULT_COLLECTOR_ENDPOINT,H as DEFAULT_FLUSH_INTERVAL_MS,B as DEFAULT_MAX_RETRIES,X as DEFAULT_SESSION_TIMEOUT_MS,b as SDK_VERSION,Jt as createTracwell};
1
+ var O="https://collect.tracwell.app/v1/events",H=25,B=1e4,X=Number.POSITIVE_INFINITY,Y=2e3,G=300*1e3,A=100,K=1800*1e3,b="0.2.0";function E(t,e){throw e?.({code:"CONFIG_INVALID",message:t}),new TypeError(t)}function oe(t,e){let n=t.trim();return n.length<20||n.length>128||!/^[A-Za-z0-9_-]+$/u.test(n)?E("projectKey does not match the public project-key contract.",e):n}function se(t,e){let n;try{n=new URL(t)}catch{return E("endpoint must be an absolute HTTPS URL.",e)}return ae(n,e)}function ae(t,e){let n=t.hostname==="localhost"||t.hostname==="127.0.0.1"||t.hostname==="[::1]";return t.protocol!=="https:"&&!n?E("endpoint must use HTTPS unless it uses a loopback host.",e):t.toString()}function y(t,e,n,r,i){return!Number.isSafeInteger(e)||e<n||e>r?E(`${t} must be an integer between ${n} and ${r}.`,i):e}function z(t){let e=oe(t.projectKey,t.onError),n=t.collectionMode??"private",r=se(t.endpoint??O,t.onError);return n==="private"&&t.persistence!==void 0&&t.persistence!=="none"?E('Private collection requires persistence: "none".',t.onError):{autoTrackPageViews:t.autoTrackPageViews??!0,batchSize:y("batchSize",t.batchSize??H,1,50,t.onError),consent:t.consent??"granted",collectionMode:n,endpoint:r,flushIntervalMs:y("flushIntervalMs",t.flushIntervalMs??B,1e3,6e4,t.onError),maxRetries:t.maxRetries===void 0?X:y("maxRetries",t.maxRetries,0,5,t.onError),persistence:n==="private"?"none":t.persistence??"localStorage",projectKey:e,respectDoNotTrack:t.respectDoNotTrack??!0,sessionTimeoutMs:y("sessionTimeoutMs",t.sessionTimeoutMs??K,6e4,1440*60*1e3,t.onError),...t.onDelivery?{onDelivery:t.onDelivery}:{},...t.onError?{onError:t.onError}:{}}}var j=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"];function de(t){let e=t?.trim();return e||void 0}function ce(t){if(t)try{return new URL(t).toString()}catch{return}}function R(t){let e=new URL(t.href),n={},r=ce(t.referrer);r&&(n.referrer=r);for(let i of j){let o=de(e.searchParams.get(i));o&&(n[i]=o)}return n}function N(t){let e=j.map(n=>t[n]??"").join("");return e.replaceAll("","")?e:void 0}var L=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu,Ie=/^[A-Za-z][A-Za-z0-9_.:-]*$/u,ge=/[\s\x00-\x1f\x7f]/u,Ae=/[\x00-\x1f\x7f]/u;function P(t){return new TextEncoder().encode(JSON.stringify(t)).byteLength}function w(t){return t.length>0&&t.length<=256&&!ge.test(t)}function F(t,e=2048){if(t.length>e)return!1;try{return new URL(t).origin.length>0}catch{return!1}}function C(t,e,n){return e>4?!1:typeof t=="string"?t.length<=1024:typeof t=="number"?Number.isFinite(t):t===null||typeof t=="boolean"?!0:Array.isArray(t)?t.length<=50&&t.every(r=>C(r,e+1,n)):Object.entries(t).every(([r,i])=>(n.count+=1,n.count<=100&&r.length>0&&r.length<=128&&!Ae.test(r)&&C(i,e+1,n)))}function J(t){let{context:e}=t;try{if(P(t)>16384)return!1}catch{return!1}return L.test(t.event_id)&&w(t.anonymous_id)&&w(t.session_id)&&(t.user_id===void 0||w(t.user_id))&&(t.event_type!=="event"||t.event_name.length<=100&&Ie.test(t.event_name)&&t.event_name!=="identify"&&t.event_name!=="page_view")&&F(e.url)&&e.path.startsWith("/")&&e.path.length<=1024&&(e.title===void 0||e.title.length<=512)&&(e.referrer===void 0||F(e.referrer))&&[e.utm_source,e.utm_medium,e.utm_campaign,e.utm_term,e.utm_content].every(n=>n===void 0||n.length>0&&n.length<=256)&&(t.properties===void 0||C(t.properties,0,{count:0}))}function $(t){if(!t||typeof t!="object")return;let e=t;if(!(e.receipt_version!==1||typeof e.receipt_id!="string"||!L.test(e.receipt_id)||typeof e.batch_id!="string"||!L.test(e.batch_id)||!Number.isInteger(e.accepted_events)||(e.accepted_events??0)<1||(e.accepted_events??0)>50||typeof e.accepted_at!="string"||Number.isNaN(Date.parse(e.accepted_at))))return e}function be(t,e){let n=new URL(t.href);return n.hash="",{path:n.pathname,url:n.toString(),...t.title?{title:t.title}:{},...e}}function q(t,e,n,r){let i={schema_version:1,event_id:t.randomUUID(),event_name:n.eventName,event_type:n.eventType,timestamp:new Date(t.now()).toISOString(),anonymous_id:e.anonymousId,session_id:e.sessionId,context:be(t.page(),e.attribution),...n.properties?{properties:n.properties}:{},...n.userId?{user_id:n.userId}:{}};if(!J(i)){r?.({code:"EVENT_INVALID",message:"Event was not queued because it failed the v1 contract."});return}return i}var Q=1;function Re(t){if(!t||typeof t!="object")return!1;let e=t;return e.version===Q&&typeof e.anonymousId=="string"&&typeof e.sessionId=="string"&&typeof e.lastActivityAt=="number"&&typeof e.attribution=="object"&&e.attribution!==null&&(e.userId===void 0||typeof e.userId=="string")}var S=class{#e;#i;#o;#t;#n;#s;constructor(e,n){this.#o=e,this.#i=n.onError,this.#t=n.sessionTimeoutMs,this.#e=`tracwell:${n.projectKey}:identity`,this.#n=n.persistence==="localStorage"?this.#h(e):void 0}identify(e,n){let r=this.#r(n);return r.userId&&r.userId!==e&&(r.anonymousId=this.#o.randomUUID(),r.sessionId=this.#o.randomUUID()),r.userId=e,this.#c(r),this.#l(r)}reset(){if(this.#s=void 0,this.#n)try{this.#n.removeItem(this.#e)}catch(e){this.#d(e)}}snapshot(e){return this.#l(this.#r(e))}#a(e){let n=this.#o.now();return{anonymousId:this.#o.randomUUID(),attribution:e,lastActivityAt:n,sessionId:this.#o.randomUUID(),version:Q}}#r(e){let n=this.#o.now(),r=this.#u();if(!r){let a=this.#a(e);return this.#c(a),a}let i=N(r.attribution),o=N(e);return(n-r.lastActivityAt>=this.#t||o!==void 0&&o!==i)&&(r.sessionId=this.#o.randomUUID(),r.attribution=e),r.lastActivityAt=n,this.#c(r),r}#u(){if(this.#s)return this.#s;if(this.#n)try{let e=this.#n.getItem(this.#e);if(!e)return;let n=JSON.parse(e);if(!Re(n)){this.#n.removeItem(this.#e);return}return this.#s=n,n}catch(e){this.#d(e);return}}#h(e){try{return e.storage()}catch(n){this.#d(n);return}}#d(e){this.#i?.({cause:e,code:"STORAGE_UNAVAILABLE",message:"Persistent identity storage is unavailable; using in-memory identity."})}#l(e){return{anonymousId:e.anonymousId,attribution:e.attribution,sessionId:e.sessionId,...e.userId?{userId:e.userId}:{}}}#c(e){if(this.#s=e,!!this.#n)try{this.#n.setItem(this.#e,JSON.stringify(e))}catch(n){this.#d(n)}}};var W=8634e4;function f(t,e,n,r){return{...r===void 0?{}:{cause:r},code:t,eventIds:n.batch.events.map(i=>i.event_id),message:e}}function Le(t){let e=JSON.parse(t??"{}");return e.version===1&&Array.isArray(e.events)?e.events.filter(n=>typeof n?.event_id=="string"&&typeof n.timestamp=="string"):[]}var v=class{#e;#i;#o;#t;#n;#s=!1;#a;#r=[];constructor(e,n){this.#i=e,this.#e=n,this.#o=n.collectionMode==="product"&&n.persistence==="localStorage"?`tracwell:${n.projectKey}:pending`:void 0}enqueue(e){if(this.#r.push(e),this.#s){this.#E(),this.#m();return}this.#r.length>=this.#e.batchSize&&this.flush()}restore(){let e=[];this.#f((r,i)=>{try{e=Le(r.getItem(i))}finally{r.removeItem(i)}});let n=this.#i.now();this.#r.unshift(...e.filter(r=>n-Date.parse(r.timestamp)<W)),this.#r.length&&this.flush()}clear(){this.#n?.(),this.#n=void 0,this.#t=void 0,this.#s=!1,this.#r=[],this.#f((e,n)=>e.removeItem(n))}flush(){return this.#a?this.#a:(this.#a=this.#u().finally(()=>{this.#a=void 0}),this.#a)}flushDue(){return this.#n?Promise.resolve():this.flush()}flushWithBeacon(){for(this.#n?.(),this.#n=void 0;this.#t||this.#r.length>0;){let e=this.#t??this.#h();if(!e)return;let n=JSON.stringify(e.batch);if(!this.#i.sendBeacon(this.#e.endpoint,n)){this.#e.onError?.(f("DELIVERY_FAILED","Browser lifecycle handoff was rejected by sendBeacon.",e));return}this.#t=void 0,this.#d({batchId:e.batch.batch_id,eventIds:e.batch.events.map(r=>r.event_id),status:"handed_off"})}}async#u(){this.#n?.(),this.#n=void 0;let e=this.#t??this.#h();if(!e)return;let n=await this.#c(e);n&&this.#r.length>0?await this.#u():n&&this.#_()}#h(){if(this.#r.length===0)return;let e=this.#r.slice(0,this.#e.batchSize),n;for(;e.length>0;){let r={batch_version:1,batch_id:this.#i.randomUUID(),project_key:this.#e.projectKey,sent_at:new Date(this.#i.now()).toISOString(),sdk:{name:"tracwell-browser",version:b},events:e};if(P(r)<=49152){n=r;break}e.pop()}if(!n){let r=this.#r.shift();r&&this.#e.onError?.({code:"EVENT_INVALID",eventIds:[r.event_id],message:"Event cannot fit in a valid browser batch."});return}return this.#r.splice(0,n.events.length),this.#t={attempt:0,batch:n},this.#t}#d(e){this.#e.onDelivery?.(e)}#l(e){let n=Math.min(Y*2**(e.attempt-1),G);this.#n=this.#i.setTimeout(()=>{this.#n=void 0,this.flush()},n)}async#c(e){let n;try{n=await this.#i.fetch(this.#e.endpoint,JSON.stringify(e.batch),"text/plain;charset=UTF-8")}catch(o){return this.#t!==e?!1:this.#p(e,o)}if(n.status!==202)return n.status===408||n.status===429||n.status>=500?this.#p(e,new Error(`Collector returned HTTP ${n.status}.`)):(this.#t===e&&(this.#t=void 0),this.#e.onError?.(f("DELIVERY_REJECTED",`Collector rejected the batch with non-retryable HTTP ${n.status}.`,e)),!1);let r;try{r=await n.json()}catch(o){return this.#e.onError?.(f("RECEIPT_INVALID","Collector returned an unreadable acceptance receipt.",e,o)),this.#p(e,o)}let i=$(r);return!i||i.batch_id!==e.batch.batch_id||i.accepted_events!==e.batch.events.length?(this.#e.onError?.(f("RECEIPT_INVALID","Collector receipt does not match the submitted batch.",e)),this.#p(e,new Error("Acceptance receipt mismatch."))):(this.#t===e&&(this.#t=void 0),this.#d({batchId:e.batch.batch_id,eventIds:e.batch.events.map(o=>o.event_id),receiptId:i.receipt_id,status:"accepted"}),!0)}#p(e,n){if(this.#t!==e)return!1;e.attempt+=1;let r=this.#i.now()-Date.parse(e.batch.events[0]?.timestamp??"")<W;return e.attempt<=this.#e.maxRetries&&r?(this.#s=!0,this.#e.onError?.(f("DELIVERY_FAILED","Event batch delivery failed; retrying.",e,n)),this.#E(),this.#m(),this.#l(e),!1):(this.#t=void 0,this.#e.onError?.(f("DELIVERY_EXHAUSTED","Event batch delivery exhausted its retries.",e,n)),this.#m(),!1)}#E(){let e=(this.#t?.batch.events.length??0)+this.#r.length,n=Math.min(e-A,this.#r.length);if(n<=0)return;let r=this.#r.splice(0,n);this.#e.onError?.({code:"DELIVERY_EXHAUSTED",eventIds:r.map(i=>i.event_id),message:"Retry buffer full; oldest events dropped."})}#_(){this.#s&&(this.#s=!1,this.#f((e,n)=>e.removeItem(n)))}#m(){let e=[...this.#t?.batch.events??[],...this.#r].slice(-A);this.#f((n,r)=>e.length?n.setItem(r,JSON.stringify({version:1,events:e})):n.removeItem(r))}#f(e){if(this.#o)try{let n=this.#i.storage();n&&e(n,this.#o)}catch{}}};function Z(t,e){let n=new S(t,e),r=new v(t,e),i=e.consent,o,h=!1,c=!1,a=[];function _(){return i==="granted"&&!(e.respectDoNotTrack&&t.doNotTrackEnabled())}function T(s,u,d,l){if(c||!_())return;let V=R(t.page()),U=n.snapshot(V),p=l??U.userId,ne=s==="identify"&&p?{eventName:"identify",eventType:"identify",...d?{properties:d}:{},userId:p}:s==="pageview"?{eventName:"page_view",eventType:"pageview",...d?{properties:d}:{},...p?{userId:p}:{}}:{eventName:u,eventType:"event",...d?{properties:d}:{},...p?{userId:p}:{}},m=q(t,U,ne,e.onError);if(m){if(s==="identify"&&l){let k=n.identify(l,V);m.anonymous_id=k.anonymousId,m.session_id=k.sessionId}return r.enqueue(m),m.event_id}}function I(s){if(!_())return;let u=new URL(t.page().href);u.hash="";let d=u.toString();if(d===o)return;let l=T("pageview","page_view",s);return l&&(o=d),l}function x(){a.forEach(s=>s()),a=[],h=!1}function D(){h||c||!_()||(h=!0,a=[t.addPageHideListener(()=>r.flushWithBeacon()),t.addVisibilityHiddenListener(()=>r.flushWithBeacon()),t.setInterval(()=>{r.flushDue()},e.flushIntervalMs)],r.restore(),e.autoTrackPageViews&&(a.push(t.subscribeToNavigation(()=>{I()})),I()))}return D(),{flush(){return r.flush()},getSession(){if(c||e.collectionMode!=="product"||!_())return;let s=n.snapshot(R(t.page()));return{anonymousId:s.anonymousId,sessionId:s.sessionId,...s.userId?{userId:s.userId}:{}}},identify(s,u){if(e.collectionMode==="private"){e.onError?.({code:"IDENTIFY_UNAVAILABLE",message:"identify() is unavailable in Private collection mode."});return}return T("identify","identify",u,s)},page:I,reset(){n.reset(),o=void 0},setConsent(s){if(i=s,s==="denied"){x(),r.clear(),n.reset(),o=void 0;return}D()},async shutdown(){c||(x(),await r.flush(),c=!0)},track(s,u){return T("event",s,u)}}}function ee(t,e){return window.addEventListener(t,e),()=>window.removeEventListener(t,e)}function te(){return{addPageHideListener(t){return ee("pagehide",t)},addVisibilityHiddenListener(t){let e=()=>{document.visibilityState==="hidden"&&t()};return document.addEventListener("visibilitychange",e),()=>document.removeEventListener("visibilitychange",e)},doNotTrackEnabled(){return navigator.doNotTrack==="1"},async fetch(t,e,n){return fetch(t,{body:e,credentials:"omit",headers:{"content-type":n},keepalive:!0,method:"POST"})},now(){return Date.now()},page(){return{href:window.location.href,referrer:document.referrer,title:document.title}},randomUUID(){return crypto.randomUUID()},sendBeacon(t,e){return navigator.sendBeacon(t,e)},setInterval(t,e){let n=window.setInterval(t,e);return()=>window.clearInterval(n)},setTimeout(t,e){let n=window.setTimeout(t,e);return()=>window.clearTimeout(n)},storage(){return window.localStorage},subscribeToNavigation(t){let e=!0,n=history.pushState,r=history.replaceState,i=()=>{e&&queueMicrotask(t)},o=function(...a){n.apply(history,a),i()},h=function(...a){r.apply(history,a),i()};history.pushState=o,history.replaceState=h;let c=ee("popstate",t);return()=>{e=!1,c(),history.pushState===o&&(history.pushState=n),history.replaceState===h&&(history.replaceState=r)}}}}function et(t){if(typeof window>"u"||typeof document>"u"||typeof navigator>"u")throw new Error("createTracwell must be called in a browser after the document is available.");return Z(te(),z(t))}export{H as DEFAULT_BATCH_SIZE,O as DEFAULT_COLLECTOR_ENDPOINT,B as DEFAULT_FLUSH_INTERVAL_MS,X as DEFAULT_MAX_RETRIES,K as DEFAULT_SESSION_TIMEOUT_MS,b as SDK_VERSION,et as createTracwell};
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "tracwell",
3
- "version": "0.4.0",
4
- "description": "Privacy-aware browser analytics and optional server-side AI crawler tracking for Tracwell",
3
+ "version": "0.6.0",
4
+ "description": "Privacy-aware browser analytics, optional server-side AI crawler tracking, and the tracwell CLI for coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
+ "bin": {
9
+ "tracwell": "./dist/npm/cli.js"
10
+ },
8
11
  "main": "./dist/npm/index.js",
9
12
  "module": "./dist/npm/index.js",
10
13
  "types": "./dist/npm/index.d.ts",
@@ -28,7 +31,9 @@
28
31
  "LICENSE",
29
32
  "README.md",
30
33
  "dist/npm/server.js",
31
- "dist/npm/server.d.ts"
34
+ "dist/npm/server.d.ts",
35
+ "dist/npm/cli.js",
36
+ "skills/tracwell/SKILL.md"
32
37
  ],
33
38
  "keywords": [
34
39
  "analytics",
@@ -41,6 +46,7 @@
41
46
  "access": "public"
42
47
  },
43
48
  "devDependencies": {
49
+ "@types/node": "26.1.1",
44
50
  "esbuild": "0.28.1",
45
51
  "publint": "0.3.24",
46
52
  "vitest": "4.1.10",
@@ -52,6 +58,6 @@
52
58
  "build:npm": "node scripts/build-npm.mjs && tsc -p tsconfig.build.json",
53
59
  "check:package": "pnpm build:npm && publint && node scripts/smoke-npm.mjs",
54
60
  "test": "vitest run",
55
- "typecheck": "tsc -p tsconfig.json"
61
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.cli.json"
56
62
  }
57
63
  }
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: tracwell
3
+ description: Add Tracwell analytics to a website or app and read its reports with the tracwell CLI. Use when the user wants analytics, page views, signups, funnels or revenue tracking set up, or asks which pages, sources or campaigns bring visitors, signups or money.
4
+ ---
5
+
6
+ # Tracwell
7
+
8
+ Tracwell is product analytics for websites and apps. The `tracwell` CLI signs
9
+ in, adds a website, prints the install snippet, checks that events arrive, and
10
+ reads reports. Every command prints JSON.
11
+
12
+ ## Quick start
13
+
14
+ ```sh
15
+ npx tracwell init --url https://acme.com
16
+ ```
17
+
18
+ `init` signs in if needed, finds or adds the website, saves it as the default,
19
+ prints the install snippet, and installs this skill plus a short block in
20
+ `AGENTS.md`. Then do step 3 and step 4 below. The steps below are the same
21
+ thing one command at a time.
22
+
23
+ ## 1. Sign in
24
+
25
+ ```sh
26
+ npx tracwell login
27
+ ```
28
+
29
+ This opens the browser. The user signs in, picks websites and approves what
30
+ the CLI may do. Wait for them; do not try to complete the page yourself.
31
+ Ticking "Add websites" is needed for step 2.
32
+
33
+ Without a browser (CI, remote shells), ask the user for an API key from
34
+ Tracwell Settings → API & MCP and set `TRACWELL_API_KEY`. Keys can read reports
35
+ but cannot add websites.
36
+
37
+ ## 2. Add the website
38
+
39
+ Skip this if `npx tracwell sites` already lists it.
40
+
41
+ ```sh
42
+ npx tracwell sites add "Acme" https://acme.com
43
+ npx tracwell use <website-id>
44
+ ```
45
+
46
+ `use` saves the website so later commands do not need `--site`.
47
+
48
+ ## 3. Install the snippet
49
+
50
+ ```sh
51
+ npx tracwell snippet
52
+ ```
53
+
54
+ Put the returned script tag in the site's `<head>` on every page, or use the
55
+ npm setup it returns for apps built with a bundler. Do not invent a project
56
+ key; always use the one from `snippet`.
57
+
58
+ ## 4. Verify
59
+
60
+ Deploy or run the site, open a page, then:
61
+
62
+ ```sh
63
+ npx tracwell verify
64
+ ```
65
+
66
+ `waiting` means no event has arrived yet. `receiving` means it works.
67
+
68
+ ## 5. Read reports
69
+
70
+ Reports default to the last 7 days (UTC). Use `--days 30`, or `--from` and
71
+ `--to` as `YYYY-MM-DD`.
72
+
73
+ ```sh
74
+ npx tracwell overview
75
+ npx tracwell acquisition --days 30
76
+ npx tracwell revenue
77
+ npx tracwell events
78
+ npx tracwell funnels
79
+ ```
80
+
81
+ ## 6. Track what matters
82
+
83
+ Needs "Manage goals and funnels" approved at sign-in. Check with `--dry-run` first.
84
+
85
+ ```sh
86
+ npx tracwell goals add "Signed up" --event signup_completed --category signup
87
+ npx tracwell goals add "Viewed pricing" --page /pricing
88
+ npx tracwell funnels add "Checkout" --steps /pricing,event:checkout_started,event:purchase_completed
89
+ ```
90
+
91
+ Steps starting with `/` are pages; `event:name` (or a bare name) is an event.
92
+ Only use event names that `npx tracwell events` shows, or that you just added
93
+ to the code. Then read them with `npx tracwell goal --goal-id <id>` or
94
+ `npx tracwell funnel --funnel-id <id>`.
95
+
96
+ ## More
97
+
98
+ - `npx tracwell help <command>` lists a command's arguments, read live from Tracwell.
99
+ - `npx tracwell search <words>` finds a tool; `npx tracwell tools` lists them all.
100
+ Run any of them with `npx tracwell call <tool> --arg value`.
101
+ - Add `--dry-run` to see the tool and arguments without sending, before any write.
102
+ - Track custom events (signups, purchases) with `window.tracwell?.track("signup_completed")`.
103
+ Docs: https://tracwell.app/docs
104
+ - Exit codes: 0 ok, 1 request failed, 2 bad input, 3 sign-in needed (run `login`).