tracwell 0.5.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 +65 -2
- package/dist/npm/cli.js +84 -0
- package/dist/npm/index.js +1 -1
- package/package.json +10 -4
- package/skills/tracwell/SKILL.md +104 -0
package/README.md
CHANGED
|
@@ -212,10 +212,21 @@ src/
|
|
|
212
212
|
types.ts Public configuration, client, delivery, and error types
|
|
213
213
|
client.test.ts Deterministic SDK behavior and failure tests
|
|
214
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
|
|
215
226
|
scripts/
|
|
216
227
|
build-cdn.mjs Minified browser build and gzip-size enforcement
|
|
217
|
-
build-npm.mjs Self-contained ESM package build
|
|
218
|
-
smoke-npm.mjs Packed runtime and
|
|
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
|
|
219
230
|
static/
|
|
220
231
|
_headers Cross-origin browser asset headers
|
|
221
232
|
```
|
|
@@ -273,3 +284,55 @@ handling because Workers does not support the fetch redirect error mode. Redirec
|
|
|
273
284
|
responses are rejected without forwarding the server key.
|
|
274
285
|
|
|
275
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`.
|
package/dist/npm/cli.js
ADDED
|
@@ -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});
|
package/dist/npm/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var O="https://collect.tracwell.app/v1/events",H=25,B=1e4,X=Number.POSITIVE_INFINITY,Y=2e3,G=300*1e3,A=100,K=1800*1e3,b="0.2.0";function E(t,e){throw e?.({code:"CONFIG_INVALID",message:t}),new TypeError(t)}function oe(t,e){let n=t.trim();return n.length<20||n.length>128||!/^[A-Za-z0-9_-]+$/u.test(n)?E("projectKey does not match the public project-key contract.",e):n}function se(t,e){let n;try{n=new URL(t)}catch{return E("endpoint must be an absolute HTTPS URL.",e)}return ae(n,e)}function ae(t,e){let n=t.hostname==="localhost"||t.hostname==="127.0.0.1"||t.hostname==="[::1]";return t.protocol!=="https:"&&!n?E("endpoint must use HTTPS unless it uses a loopback host.",e):t.toString()}function y(t,e,n,r,i){return!Number.isSafeInteger(e)||e<n||e>r?E(`${t} must be an integer between ${n} and ${r}.`,i):e}function z(t){let e=oe(t.projectKey,t.onError),n=t.collectionMode??"private",r=se(t.endpoint??O,t.onError);return n==="private"&&t.persistence!==void 0&&t.persistence!=="none"?E('Private collection requires persistence: "none".',t.onError):{autoTrackPageViews:t.autoTrackPageViews??!0,batchSize:y("batchSize",t.batchSize??H,1,50,t.onError),consent:t.consent??"granted",collectionMode:n,endpoint:r,flushIntervalMs:y("flushIntervalMs",t.flushIntervalMs??B,1e3,6e4,t.onError),maxRetries:t.maxRetries===void 0?X:y("maxRetries",t.maxRetries,0,5,t.onError),persistence:n==="private"?"none":t.persistence??"localStorage",projectKey:e,respectDoNotTrack:t.respectDoNotTrack??!0,sessionTimeoutMs:y("sessionTimeoutMs",t.sessionTimeoutMs??K,6e4,1440*60*1e3,t.onError),...t.onDelivery?{onDelivery:t.onDelivery}:{},...t.onError?{onError:t.onError}:{}}}var j=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"];function de(t){let e=t?.trim();return e||void 0}function ce(t){if(t)try{return new URL(t).toString()}catch{return}}function R(t){let e=new URL(t.href),n={},r=ce(t.referrer);r&&(n.referrer=r);for(let i of j){let o=de(e.searchParams.get(i));o&&(n[i]=o)}return n}function N(t){let e=j.map(n=>t[n]??"").join("");return e.replaceAll("","")?e:void 0}var L=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu,Ie=/^[A-Za-z][A-Za-z0-9_.:-]*$/u,ge=/[\s\x00-\x1f\x7f]/u,Ae=/[\x00-\x1f\x7f]/u;function P(t){return new TextEncoder().encode(JSON.stringify(t)).byteLength}function w(t){return t.length>0&&t.length<=256&&!ge.test(t)}function F(t,e=2048){if(t.length>e)return!1;try{return new URL(t).origin.length>0}catch{return!1}}function C(t,e,n){return e>4?!1:typeof t=="string"?t.length<=1024:typeof t=="number"?Number.isFinite(t):t===null||typeof t=="boolean"?!0:Array.isArray(t)?t.length<=50&&t.every(r=>C(r,e+1,n)):Object.entries(t).every(([r,i])=>(n.count+=1,n.count<=100&&r.length>0&&r.length<=128&&!Ae.test(r)&&C(i,e+1,n)))}function J(t){let{context:e}=t;try{if(P(t)>16384)return!1}catch{return!1}return L.test(t.event_id)&&w(t.anonymous_id)&&w(t.session_id)&&(t.user_id===void 0||w(t.user_id))&&(t.event_type!=="event"||t.event_name.length<=100&&Ie.test(t.event_name)&&t.event_name!=="identify"&&t.event_name!=="page_view")&&F(e.url)&&e.path.startsWith("/")&&e.path.length<=1024&&(e.title===void 0||e.title.length<=512)&&(e.referrer===void 0||F(e.referrer))&&[e.utm_source,e.utm_medium,e.utm_campaign,e.utm_term,e.utm_content].every(n=>n===void 0||n.length>0&&n.length<=256)&&(t.properties===void 0||C(t.properties,0,{count:0}))}function $(t){if(!t||typeof t!="object")return;let e=t;if(!(e.receipt_version!==1||typeof e.receipt_id!="string"||!L.test(e.receipt_id)||typeof e.batch_id!="string"||!L.test(e.batch_id)||!Number.isInteger(e.accepted_events)||(e.accepted_events??0)<1||(e.accepted_events??0)>50||typeof e.accepted_at!="string"||Number.isNaN(Date.parse(e.accepted_at))))return e}function be(t,e){let n=new URL(t.href);return n.hash="",{path:n.pathname,url:n.toString(),...t.title?{title:t.title}:{},...e}}function q(t,e,n,r){let i={schema_version:1,event_id:t.randomUUID(),event_name:n.eventName,event_type:n.eventType,timestamp:new Date(t.now()).toISOString(),anonymous_id:e.anonymousId,session_id:e.sessionId,context:be(t.page(),e.attribution),...n.properties?{properties:n.properties}:{},...n.userId?{user_id:n.userId}:{}};if(!J(i)){r?.({code:"EVENT_INVALID",message:"Event was not queued because it failed the v1 contract."});return}return i}var Q=1;function Re(t){if(!t||typeof t!="object")return!1;let e=t;return e.version===Q&&typeof e.anonymousId=="string"&&typeof e.sessionId=="string"&&typeof e.lastActivityAt=="number"&&typeof e.attribution=="object"&&e.attribution!==null&&(e.userId===void 0||typeof e.userId=="string")}var v=class{#e;#i;#o;#t;#n;#s;constructor(e,n){this.#o=e,this.#i=n.onError,this.#t=n.sessionTimeoutMs,this.#e=`tracwell:${n.projectKey}:identity`,this.#n=n.persistence==="localStorage"?this.#h(e):void 0}identify(e,n){let r=this.#r(n);return r.userId&&r.userId!==e&&(r.anonymousId=this.#o.randomUUID(),r.sessionId=this.#o.randomUUID()),r.userId=e,this.#c(r),this.#l(r)}reset(){if(this.#s=void 0,this.#n)try{this.#n.removeItem(this.#e)}catch(e){this.#d(e)}}snapshot(e){return this.#l(this.#r(e))}#a(e){let n=this.#o.now();return{anonymousId:this.#o.randomUUID(),attribution:e,lastActivityAt:n,sessionId:this.#o.randomUUID(),version:Q}}#r(e){let n=this.#o.now(),r=this.#u();if(!r){let a=this.#a(e);return this.#c(a),a}let i=N(r.attribution),o=N(e);return(n-r.lastActivityAt>=this.#t||o!==void 0&&o!==i)&&(r.sessionId=this.#o.randomUUID(),r.attribution=e),r.lastActivityAt=n,this.#c(r),r}#u(){if(this.#s)return this.#s;if(this.#n)try{let e=this.#n.getItem(this.#e);if(!e)return;let n=JSON.parse(e);if(!Re(n)){this.#n.removeItem(this.#e);return}return this.#s=n,n}catch(e){this.#d(e);return}}#h(e){try{return e.storage()}catch(n){this.#d(n);return}}#d(e){this.#i?.({cause:e,code:"STORAGE_UNAVAILABLE",message:"Persistent identity storage is unavailable; using in-memory identity."})}#l(e){return{anonymousId:e.anonymousId,attribution:e.attribution,sessionId:e.sessionId,...e.userId?{userId:e.userId}:{}}}#c(e){if(this.#s=e,!!this.#n)try{this.#n.setItem(this.#e,JSON.stringify(e))}catch(n){this.#d(n)}}};var W=8634e4;function f(t,e,n,r){return{...r===void 0?{}:{cause:r},code:t,eventIds:n.batch.events.map(i=>i.event_id),message:e}}function Le(t){let e=JSON.parse(t??"{}");return e.version===1&&Array.isArray(e.events)?e.events.filter(n=>typeof n?.event_id=="string"&&typeof n.timestamp=="string"):[]}var S=class{#e;#i;#o;#t;#n;#s=!1;#a;#r=[];constructor(e,n){this.#i=e,this.#e=n,this.#o=n.collectionMode==="product"&&n.persistence==="localStorage"?`tracwell:${n.projectKey}:pending`:void 0}enqueue(e){if(this.#r.push(e),this.#s){this.#E(),this.#m();return}this.#r.length>=this.#e.batchSize&&this.flush()}restore(){let e=[];this.#f((r,i)=>{try{e=Le(r.getItem(i))}finally{r.removeItem(i)}});let n=this.#i.now();this.#r.unshift(...e.filter(r=>n-Date.parse(r.timestamp)<W)),this.#r.length&&this.flush()}clear(){this.#n?.(),this.#n=void 0,this.#t=void 0,this.#s=!1,this.#r=[],this.#f((e,n)=>e.removeItem(n))}flush(){return this.#a?this.#a:(this.#a=this.#u().finally(()=>{this.#a=void 0}),this.#a)}flushDue(){return this.#n?Promise.resolve():this.flush()}flushWithBeacon(){for(this.#n?.(),this.#n=void 0;this.#t||this.#r.length>0;){let e=this.#t??this.#h();if(!e)return;let n=JSON.stringify(e.batch);if(!this.#i.sendBeacon(this.#e.endpoint,n)){this.#e.onError?.(f("DELIVERY_FAILED","Browser lifecycle handoff was rejected by sendBeacon.",e));return}this.#t=void 0,this.#d({batchId:e.batch.batch_id,eventIds:e.batch.events.map(r=>r.event_id),status:"handed_off"})}}async#u(){this.#n?.(),this.#n=void 0;let e=this.#t??this.#h();if(!e)return;let n=await this.#c(e);n&&this.#r.length>0?await this.#u():n&&this.#_()}#h(){if(this.#r.length===0)return;let e=this.#r.slice(0,this.#e.batchSize),n;for(;e.length>0;){let r={batch_version:1,batch_id:this.#i.randomUUID(),project_key:this.#e.projectKey,sent_at:new Date(this.#i.now()).toISOString(),sdk:{name:"tracwell-browser",version:b},events:e};if(P(r)<=49152){n=r;break}e.pop()}if(!n){let r=this.#r.shift();r&&this.#e.onError?.({code:"EVENT_INVALID",eventIds:[r.event_id],message:"Event cannot fit in a valid browser batch."});return}return this.#r.splice(0,n.events.length),this.#t={attempt:0,batch:n},this.#t}#d(e){this.#e.onDelivery?.(e)}#l(e){let n=Math.min(Y*2**(e.attempt-1),G);this.#n=this.#i.setTimeout(()=>{this.#n=void 0,this.flush()},n)}async#c(e){let n;try{n=await this.#i.fetch(this.#e.endpoint,JSON.stringify(e.batch),"text/plain;charset=UTF-8")}catch(o){return this.#t!==e?!1:this.#p(e,o)}if(n.status!==202)return n.status===408||n.status===429||n.status>=500?this.#p(e,new Error(`Collector returned HTTP ${n.status}.`)):(this.#t===e&&(this.#t=void 0),this.#e.onError?.(f("DELIVERY_REJECTED",`Collector rejected the batch with non-retryable HTTP ${n.status}.`,e)),!1);let r;try{r=await n.json()}catch(o){return this.#e.onError?.(f("RECEIPT_INVALID","Collector returned an unreadable acceptance receipt.",e,o)),this.#p(e,o)}let i=$(r);return!i||i.batch_id!==e.batch.batch_id||i.accepted_events!==e.batch.events.length?(this.#e.onError?.(f("RECEIPT_INVALID","Collector receipt does not match the submitted batch.",e)),this.#p(e,new Error("Acceptance receipt mismatch."))):(this.#t===e&&(this.#t=void 0),this.#d({batchId:e.batch.batch_id,eventIds:e.batch.events.map(o=>o.event_id),receiptId:i.receipt_id,status:"accepted"}),!0)}#p(e,n){if(this.#t!==e)return!1;e.attempt+=1;let r=this.#i.now()-Date.parse(e.batch.events[0]?.timestamp??"")<W;return e.attempt<=this.#e.maxRetries&&r?(this.#s=!0,this.#e.onError?.(f("DELIVERY_FAILED","Event batch delivery failed; retrying.",e,n)),this.#E(),this.#m(),this.#l(e),!1):(this.#t=void 0,this.#e.onError?.(f("DELIVERY_EXHAUSTED","Event batch delivery exhausted its retries.",e,n)),this.#m(),!1)}#E(){let e=(this.#t?.batch.events.length??0)+this.#r.length,n=Math.min(e-A,this.#r.length);if(n<=0)return;let r=this.#r.splice(0,n);this.#e.onError?.({code:"DELIVERY_EXHAUSTED",eventIds:r.map(i=>i.event_id),message:"Retry buffer full; oldest events dropped."})}#_(){this.#s&&(this.#s=!1,this.#f((e,n)=>e.removeItem(n)))}#m(){let e=[...this.#t?.batch.events??[],...this.#r].slice(-A);this.#f((n,r)=>e.length?n.setItem(r,JSON.stringify({version:1,events:e})):n.removeItem(r))}#f(e){if(this.#o)try{let n=this.#i.storage();n&&e(n,this.#o)}catch{}}};function Z(t,e){let n=new v(t,e),r=new S(t,e),i=e.consent,o,h=!1,c=!1,a=[];function _(){return i==="granted"&&!(e.respectDoNotTrack&&t.doNotTrackEnabled())}function T(s,u,d,l){if(c||!_())return;let V=R(t.page()),U=n.snapshot(V),p=l??U.userId,ne=s==="identify"&&p?{eventName:"identify",eventType:"identify",...d?{properties:d}:{},userId:p}:s==="pageview"?{eventName:"page_view",eventType:"pageview",...d?{properties:d}:{},...p?{userId:p}:{}}:{eventName:u,eventType:"event",...d?{properties:d}:{},...p?{userId:p}:{}},m=q(t,U,ne,e.onError);if(m){if(s==="identify"&&l){let k=n.identify(l,V);m.anonymous_id=k.anonymousId,m.session_id=k.sessionId}return r.enqueue(m),m.event_id}}function I(s){if(!_())return;let u=new URL(t.page().href);u.hash="";let d=u.toString();if(d===o)return;let l=T("pageview","page_view",s);return l&&(o=d),l}function x(){a.forEach(s=>s()),a=[],h=!1}function D(){h||c||!_()||(h=!0,a=[t.addPageHideListener(()=>r.flushWithBeacon()),t.addVisibilityHiddenListener(()=>r.flushWithBeacon()),t.setInterval(()=>{r.flushDue()},e.flushIntervalMs)],r.restore(),e.autoTrackPageViews&&(a.push(t.subscribeToNavigation(()=>{I()})),I()))}return D(),{flush(){return r.flush()},getSession(){if(c||e.collectionMode!=="product"||!_())return;let s=n.snapshot(R(t.page()));return{anonymousId:s.anonymousId,sessionId:s.sessionId,...s.userId?{userId:s.userId}:{}}},identify(s,u){if(e.collectionMode==="private"){e.onError?.({code:"IDENTIFY_UNAVAILABLE",message:"identify() is unavailable in Private collection mode."});return}return T("identify","identify",u,s)},page:I,reset(){n.reset(),o=void 0},setConsent(s){if(i=s,s==="denied"){x(),r.clear(),n.reset(),o=void 0;return}D()},async shutdown(){c||(x(),await r.flush(),c=!0)},track(s,u){return T("event",s,u)}}}function ee(t,e){return window.addEventListener(t,e),()=>window.removeEventListener(t,e)}function te(){return{addPageHideListener(t){return ee("pagehide",t)},addVisibilityHiddenListener(t){let e=()=>{document.visibilityState==="hidden"&&t()};return document.addEventListener("visibilitychange",e),()=>document.removeEventListener("visibilitychange",e)},doNotTrackEnabled(){return navigator.doNotTrack==="1"},async fetch(t,e,n){return fetch(t,{body:e,credentials:"omit",headers:{"content-type":n},keepalive:!0,method:"POST"})},now(){return Date.now()},page(){return{href:window.location.href,referrer:document.referrer,title:document.title}},randomUUID(){return crypto.randomUUID()},sendBeacon(t,e){return navigator.sendBeacon(t,e)},setInterval(t,e){let n=window.setInterval(t,e);return()=>window.clearInterval(n)},setTimeout(t,e){let n=window.setTimeout(t,e);return()=>window.clearTimeout(n)},storage(){return window.localStorage},subscribeToNavigation(t){let e=!0,n=history.pushState,r=history.replaceState,i=()=>{e&&queueMicrotask(t)},o=function(...a){n.apply(history,a),i()},h=function(...a){r.apply(history,a),i()};history.pushState=o,history.replaceState=h;let c=ee("popstate",t);return()=>{e=!1,c(),history.pushState===o&&(history.pushState=n),history.replaceState===h&&(history.replaceState=r)}}}}function et(t){if(typeof window>"u"||typeof document>"u"||typeof navigator>"u")throw new Error("createTracwell must be called in a browser after the document is available.");return Z(te(),z(t))}export{H as DEFAULT_BATCH_SIZE,O as DEFAULT_COLLECTOR_ENDPOINT,B as DEFAULT_FLUSH_INTERVAL_MS,X as DEFAULT_MAX_RETRIES,K as DEFAULT_SESSION_TIMEOUT_MS,b as SDK_VERSION,et as createTracwell};
|
|
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
|
-
"description": "Privacy-aware browser analytics
|
|
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`).
|