tracwell 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -6
- package/dist/npm/config.d.ts +1 -1
- package/dist/npm/index.d.ts +1 -1
- package/dist/npm/index.js +1 -1
- package/dist/npm/server.d.ts +18 -0
- package/dist/npm/server.js +1 -0
- package/dist/npm/types.d.ts +6 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -13,12 +13,33 @@ The production browser artifact is served from the existing Collector domain:
|
|
|
13
13
|
src="https://collect.tracwell.app/script.js"
|
|
14
14
|
data-project-key="tw_live_..."
|
|
15
15
|
data-collection-mode="private"
|
|
16
|
-
data-consent="granted"
|
|
17
|
-
data-respect-do-not-track="true"
|
|
18
16
|
></script>
|
|
19
17
|
```
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
### Optional script attributes
|
|
20
|
+
|
|
21
|
+
| Attribute | Values | Default |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `data-project-key` | Your public project key (never a server key) | Required |
|
|
24
|
+
| `data-collection-mode` | `private`, `product` | `private` |
|
|
25
|
+
| `data-consent` | `granted`, `required`, `denied` | `granted` |
|
|
26
|
+
| `data-respect-do-not-track` | `true`, `false` | `true` |
|
|
27
|
+
|
|
28
|
+
Keep collection mode explicit and aligned with website settings. The generated
|
|
29
|
+
snippet omits default consent and Do Not Track attributes. Add
|
|
30
|
+
`data-consent="required"` to wait for consent before identity creation or event
|
|
31
|
+
collection. After the script loads, forward the visitor's saved choice and
|
|
32
|
+
subsequent changes using `window.tracwell?.setConsent("granted")` or
|
|
33
|
+
`window.tracwell?.setConsent("denied")`. An early call before the script loads
|
|
34
|
+
has no effect; initialize the consent integration after SDK load.
|
|
35
|
+
|
|
36
|
+
Only these four data attributes are supported. Use the npm SDK for other
|
|
37
|
+
initialization options, including batching, persistence overrides, and a custom
|
|
38
|
+
collector URL. The script reads attributes at load time and does not fetch
|
|
39
|
+
configuration from the dashboard; update the installed snippet when changing
|
|
40
|
+
website settings.
|
|
41
|
+
|
|
42
|
+
When consent and Do Not Track permit collection, loading the script captures the initial page view and exposes the same client
|
|
22
43
|
as `window.tracwell` for custom events:
|
|
23
44
|
|
|
24
45
|
```js
|
|
@@ -27,7 +48,7 @@ window.tracwell?.track("signup_completed", { plan: "starter" });
|
|
|
27
48
|
|
|
28
49
|
The script derives `/v1/events` from its own origin, so the same artifact works
|
|
29
50
|
against the loopback Collector during local development. The production bundle
|
|
30
|
-
reports SDK version `0.
|
|
51
|
+
reports SDK version `0.2.0`, targets ES2022-capable browsers, and has a 4.7 KB
|
|
31
52
|
gzip budget enforced by
|
|
32
53
|
`build:cdn`.
|
|
33
54
|
|
|
@@ -49,12 +70,32 @@ const tracwell = createTracwell({
|
|
|
49
70
|
|
|
50
71
|
tracwell.track("signup_completed", { plan: "starter" });
|
|
51
72
|
tracwell.identify("customer_123");
|
|
73
|
+
|
|
74
|
+
const session = tracwell.getSession();
|
|
52
75
|
```
|
|
53
76
|
|
|
54
77
|
`createTracwell()` starts collection immediately in the browser and captures
|
|
55
78
|
the initial page view by default. It must be called after the document is
|
|
56
79
|
available, not during server rendering.
|
|
57
80
|
|
|
81
|
+
## Framework compatibility
|
|
82
|
+
|
|
83
|
+
The npm SDK is a framework-neutral ESM browser library. It has no React, Vue,
|
|
84
|
+
Svelte, Angular, router, or rendering-framework dependency. Importing it during
|
|
85
|
+
SSR is safe; call `createTracwell()` from that framework's browser mount or
|
|
86
|
+
hydration lifecycle after `document` is available.
|
|
87
|
+
|
|
88
|
+
Multi-page applications capture a page view each time the browser loads the
|
|
89
|
+
application. Single-page applications using the standard History API receive
|
|
90
|
+
automatic page views for `pushState`, `replaceState`, and back/forward
|
|
91
|
+
navigation. This covers the default path-based routers in modern web
|
|
92
|
+
frameworks. Hash fragments remain intentionally excluded from analytics URLs;
|
|
93
|
+
hash-only navigation does not create a page view.
|
|
94
|
+
|
|
95
|
+
Framework adapters should own only lifecycle integration and dependency
|
|
96
|
+
injection. Event validation, identity, attribution, navigation observation,
|
|
97
|
+
batching, retries, consent, and delivery remain in this core package.
|
|
98
|
+
|
|
58
99
|
## Responsibilities
|
|
59
100
|
|
|
60
101
|
- Generate stable event and batch IDs before delivery.
|
|
@@ -65,8 +106,9 @@ available, not during server rendering.
|
|
|
65
106
|
- Retry bounded transient failures without changing event IDs.
|
|
66
107
|
- Respect configured consent and Do Not Track behavior before collection.
|
|
67
108
|
|
|
68
|
-
The SDK must remain browser-only, small, non-blocking, and unable
|
|
69
|
-
collector fields. It must not contain secrets or connect to
|
|
109
|
+
The default SDK entry must remain browser-only, small, non-blocking, and unable
|
|
110
|
+
to set trusted collector fields. It must not contain secrets or connect to
|
|
111
|
+
ClickHouse. The separate server entry accepts a server-side key as documented below.
|
|
70
112
|
|
|
71
113
|
## Identity and attribution
|
|
72
114
|
|
|
@@ -90,6 +132,17 @@ Collection mode is explicit:
|
|
|
90
132
|
reported as external acquisition.
|
|
91
133
|
- `identify()` validates the supplied opaque user ID before associating it with
|
|
92
134
|
future events.
|
|
135
|
+
- Identifying a different user on the same installation rotates both anonymous
|
|
136
|
+
identity and session before recording the new identity. Call `reset()` on
|
|
137
|
+
logout so an anonymous post-logout visit also starts cleanly.
|
|
138
|
+
- `getSession()` returns the active Product-mode `anonymousId`, `sessionId`,
|
|
139
|
+
and optional `userId` so a server-created checkout can carry the same
|
|
140
|
+
identifiers into an authenticated revenue event. It returns `undefined` in
|
|
141
|
+
Private mode, without consent, when Do Not Track blocks collection, or after
|
|
142
|
+
shutdown.
|
|
143
|
+
- Treat the returned IDs as attribution context, not authorization. Revenue
|
|
144
|
+
`user_id` must come from the authenticated account or verified payment
|
|
145
|
+
webhook rather than a client-supplied value.
|
|
93
146
|
- Hash-only URL changes do not create duplicate SPA page views.
|
|
94
147
|
- URL fragments are excluded from captured URLs and `path` contains only the
|
|
95
148
|
pathname.
|
|
@@ -148,6 +201,7 @@ src/
|
|
|
148
201
|
runtime.ts Browser API adapter
|
|
149
202
|
types.ts Public configuration, client, delivery, and error types
|
|
150
203
|
client.test.ts Deterministic SDK behavior and failure tests
|
|
204
|
+
index.test.ts SSR import and browser-initialization boundary
|
|
151
205
|
scripts/
|
|
152
206
|
build-cdn.mjs Minified browser build and gzip-size enforcement
|
|
153
207
|
build-npm.mjs Self-contained ESM package build
|
|
@@ -181,3 +235,25 @@ pnpm --filter {packages/sdk} test
|
|
|
181
235
|
pnpm --filter {packages/sdk} build:cdn
|
|
182
236
|
pnpm --filter {packages/sdk} check:package
|
|
183
237
|
```
|
|
238
|
+
|
|
239
|
+
## Server-only AI crawler entry
|
|
240
|
+
|
|
241
|
+
`tracwell/server` exports `trackAiCrawler(request, { serverKey, statusCode?,
|
|
242
|
+
collectorUrl? })`. Import only on your backend; never expose `tw_sk_`
|
|
243
|
+
keys in browser code. This separate bundle adds nothing to the default browser
|
|
244
|
+
entry or CDN script. Available starting with npm package version 0.3.0. The unchanged browser
|
|
245
|
+
transport continues reporting browser SDK version 0.2.0.
|
|
246
|
+
|
|
247
|
+
Call from Next.js proxy/middleware or another server/edge handler. Pass the
|
|
248
|
+
promise to the hosting runtime's `waitUntil` (and inspect its result), or await
|
|
249
|
+
on a persistent server. Keep existing request handling intact. The helper ignores
|
|
250
|
+
ordinary traffic, API paths and common static assets. Robots, sitemap and Markdown
|
|
251
|
+
requests remain trackable. Handler/cache placement determines coverage.
|
|
252
|
+
|
|
253
|
+
Results are `ignored`, `accepted` (matching Queue receipt), or `failed` with a
|
|
254
|
+
non-sensitive error code. Three total attempts use the same event ID, timestamp
|
|
255
|
+
and sanitized URL. Retries are in memory only. Do not treat acceptance as confirmed
|
|
256
|
+
database persistence. Queries/fragments are stripped; no IP, cookies or other
|
|
257
|
+
request headers are sent. Raw UA is sent solely for collector classification and
|
|
258
|
+
is not stored. Current provider matches are user-agent based, not IP verified.
|
|
259
|
+
See the public `/docs/ai-crawlers` guide for setup and direct POST integration.
|
package/dist/npm/config.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare const DEFAULT_BATCH_SIZE = 25;
|
|
|
4
4
|
export declare const DEFAULT_FLUSH_INTERVAL_MS = 10000;
|
|
5
5
|
export declare const DEFAULT_MAX_RETRIES = 2;
|
|
6
6
|
export declare const DEFAULT_SESSION_TIMEOUT_MS: number;
|
|
7
|
-
export declare const SDK_VERSION = "0.
|
|
7
|
+
export declare const SDK_VERSION = "0.2.0";
|
|
8
8
|
export declare function normalizeProjectKey(value: string, onError?: (error: SdkError) => void): string;
|
|
9
9
|
export declare function normalizeCollectorEndpoint(value: string, onError?: (error: SdkError) => void): string;
|
|
10
10
|
export declare function normalizeCollectorUrl(endpoint: URL, onError?: (error: SdkError) => void): string;
|
package/dist/npm/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { DEFAULT_BATCH_SIZE, DEFAULT_COLLECTOR_ENDPOINT, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_MAX_RETRIES, DEFAULT_SESSION_TIMEOUT_MS, SDK_VERSION, } from "./config";
|
|
2
|
-
export type { ContractIssue, ContractIssueCode, ConsentState, DeliveryReport, EventProperties, JsonPrimitive, JsonValue, SdkError, SdkErrorCode, TracwellClient, TracwellConfig, } from "./types";
|
|
2
|
+
export type { ContractIssue, ContractIssueCode, ConsentState, DeliveryReport, EventProperties, JsonPrimitive, JsonValue, SdkError, SdkErrorCode, TracwellClient, TracwellConfig, TracwellSession, } from "./types";
|
|
3
3
|
export declare function createTracwell(config: import("./types").TracwellConfig): import("./types").TracwellClient;
|
package/dist/npm/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var D="https://collect.tracwell.app/v1/events",k=25,U=1e4,O=2,H=1800*1e3,b="0.1.0";function m(e,t){throw t?.({code:"CONFIG_INVALID",message:e}),new TypeError(e)}function tt(e,t){let n=e.trim();return n.length<20||n.length>128||!/^[A-Za-z0-9_-]+$/u.test(n)?m("projectKey does not match the public project-key contract.",t):n}function et(e,t){let n;try{n=new URL(e)}catch{return m("endpoint must be an absolute HTTPS URL.",t)}return nt(n,t)}function nt(e,t){let n=e.hostname==="localhost"||e.hostname==="127.0.0.1"||e.hostname==="[::1]";return e.protocol!=="https:"&&!n?m("endpoint must use HTTPS unless it uses a loopback host.",t):e.toString()}function E(e,t,n,r,i){return!Number.isSafeInteger(t)||t<n||t>r?m(`${e} must be an integer between ${n} and ${r}.`,i):t}function B(e){let t=tt(e.projectKey,e.onError),n=e.collectionMode??"private",r=et(e.endpoint??D,e.onError);return n==="private"&&e.persistence!==void 0&&e.persistence!=="none"?m('Private collection requires persistence: "none".',e.onError):{autoTrackPageViews:e.autoTrackPageViews??!0,batchSize:E("batchSize",e.batchSize??k,1,50,e.onError),consent:e.consent??"granted",collectionMode:n,endpoint:r,flushIntervalMs:E("flushIntervalMs",e.flushIntervalMs??U,1e3,6e4,e.onError),maxRetries:E("maxRetries",e.maxRetries??O,0,5,e.onError),persistence:n==="private"?"none":e.persistence??"localStorage",projectKey:t,respectDoNotTrack:e.respectDoNotTrack??!0,sessionTimeoutMs:E("sessionTimeoutMs",e.sessionTimeoutMs??H,6e4,1440*60*1e3,e.onError),...e.onDelivery?{onDelivery:e.onDelivery}:{},...e.onError?{onError:e.onError}:{}}}var X=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"];function rt(e){let t=e?.trim();return t||void 0}function it(e){if(e)try{return new URL(e).toString()}catch{return}}function G(e){let t=new URL(e.href),n={},r=it(e.referrer);r&&(n.referrer=r);for(let i of X){let o=rt(t.searchParams.get(i));o&&(n[i]=o)}return n}function A(e){let t=X.map(n=>e[n]??"").join("");return t.replaceAll("","")?t:void 0}var N=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu,_t=/^[A-Za-z][A-Za-z0-9_.:-]*$/u,yt=/[\s\x00-\x1f\x7f]/u,Tt=/[\x00-\x1f\x7f]/u;function L(e){return new TextEncoder().encode(JSON.stringify(e)).byteLength}function R(e){return e.length>0&&e.length<=256&&!yt.test(e)}function Y(e,t=2048){if(e.length>t)return!1;try{return new URL(e).origin.length>0}catch{return!1}}function w(e,t,n){return t>4?!1:typeof e=="string"?e.length<=1024:typeof e=="number"?Number.isFinite(e):e===null||typeof e=="boolean"?!0:Array.isArray(e)?e.length<=50&&e.every(r=>w(r,t+1,n)):Object.entries(e).every(([r,i])=>(n.count+=1,n.count<=100&&r.length>0&&r.length<=128&&!Tt.test(r)&&w(i,t+1,n)))}function K(e){let{context:t}=e;try{if(L(e)>16384)return!1}catch{return!1}return N.test(e.event_id)&&R(e.anonymous_id)&&R(e.session_id)&&(e.user_id===void 0||R(e.user_id))&&(e.event_type!=="event"||e.event_name.length<=100&&_t.test(e.event_name)&&e.event_name!=="identify"&&e.event_name!=="page_view")&&Y(t.url)&&t.path.startsWith("/")&&t.path.length<=1024&&(t.title===void 0||t.title.length<=512)&&(t.referrer===void 0||Y(t.referrer))&&[t.utm_source,t.utm_medium,t.utm_campaign,t.utm_term,t.utm_content].every(n=>n===void 0||n.length>0&&n.length<=256)&&(e.properties===void 0||w(e.properties,0,{count:0}))}function z(e){if(!e||typeof e!="object")return;let t=e;if(!(t.receipt_version!==1||typeof t.receipt_id!="string"||!N.test(t.receipt_id)||typeof t.batch_id!="string"||!N.test(t.batch_id)||!Number.isInteger(t.accepted_events)||(t.accepted_events??0)<1||(t.accepted_events??0)>50||typeof t.accepted_at!="string"||Number.isNaN(Date.parse(t.accepted_at))))return t}function St(e,t){let n=new URL(e.href);return n.hash="",{path:n.pathname,url:n.toString(),...e.title?{title:e.title}:{},...t}}function j(e,t,n,r){let i={schema_version:1,event_id:e.randomUUID(),event_name:n.eventName,event_type:n.eventType,timestamp:new Date(e.now()).toISOString(),anonymous_id:t.anonymousId,session_id:t.sessionId,context:St(e.page(),t.attribution),...n.properties?{properties:n.properties}:{},...n.userId?{user_id:n.userId}:{}};if(!K(i)){r?.({code:"EVENT_INVALID",message:"Event was not queued because it failed the v1 contract."});return}return i}var F=1;function gt(e){if(!e||typeof e!="object")return!1;let t=e;return t.version===F&&typeof t.anonymousId=="string"&&typeof t.sessionId=="string"&&typeof t.lastActivityAt=="number"&&typeof t.attribution=="object"&&t.attribution!==null&&(t.userId===void 0||typeof t.userId=="string")}var _=class{#t;#o;#e;#i;#r;#n;constructor(t,n){this.#e=t,this.#o=n.onError,this.#i=n.sessionTimeoutMs,this.#t=`tracwell:${n.projectKey}:identity`,this.#r=n.persistence==="localStorage"?this.#p(t):void 0}identify(t,n){let r=this.#d(n);return r.userId=t,this.#h(r),this.#s(r)}reset(){if(this.#n=void 0,this.#r)try{this.#r.removeItem(this.#t)}catch(t){this.#a(t)}}snapshot(t){return this.#s(this.#d(t))}#c(t){let n=this.#e.now();return{anonymousId:this.#e.randomUUID(),attribution:t,lastActivityAt:n,sessionId:this.#e.randomUUID(),version:F}}#d(t){let n=this.#e.now(),r=this.#u();if(!r){let a=this.#c(t);return this.#h(a),a}let i=A(r.attribution),o=A(t);return(n-r.lastActivityAt>=this.#i||o!==void 0&&o!==i)&&(r.sessionId=this.#e.randomUUID(),r.attribution=t),r.lastActivityAt=n,this.#h(r),r}#u(){if(this.#n)return this.#n;if(this.#r)try{let t=this.#r.getItem(this.#t);if(!t)return;let n=JSON.parse(t);if(!gt(n)){this.#r.removeItem(this.#t);return}return this.#n=n,n}catch(t){this.#a(t);return}}#p(t){try{return t.storage()}catch(n){this.#a(n);return}}#a(t){this.#o?.({cause:t,code:"STORAGE_UNAVAILABLE",message:"Persistent identity storage is unavailable; using in-memory identity."})}#s(t){return{anonymousId:t.anonymousId,attribution:t.attribution,sessionId:t.sessionId,...t.userId?{userId:t.userId}:{}}}#h(t){if(this.#n=t,!!this.#r)try{this.#r.setItem(this.#t,JSON.stringify(t))}catch(n){this.#a(n)}}};function f(e,t,n,r){return{...r===void 0?{}:{cause:r},code:e,eventIds:n.batch.events.map(i=>i.event_id),message:t}}var y=class{#t;#o;#e;#i;#r;#n=[];constructor(t,n){this.#o=t,this.#t=n}enqueue(t){this.#n.push(t),this.#n.length>=this.#t.batchSize&&this.flush()}clear(){this.#i?.(),this.#i=void 0,this.#e=void 0,this.#n=[]}flush(){return this.#r?this.#r:(this.#r=this.#c().finally(()=>{this.#r=void 0}),this.#r)}flushWithBeacon(){for(this.#i?.(),this.#i=void 0;this.#e||this.#n.length>0;){let t=this.#e??this.#d();if(!t)return;let n=JSON.stringify(t.batch);if(!this.#o.sendBeacon(this.#t.endpoint,n)){this.#t.onError?.(f("DELIVERY_FAILED","Browser lifecycle handoff was rejected by sendBeacon.",t));return}this.#e=void 0,this.#u({batchId:t.batch.batch_id,eventIds:t.batch.events.map(r=>r.event_id),status:"handed_off"})}}async#c(){this.#i?.(),this.#i=void 0;let t=this.#e??this.#d();if(!t)return;await this.#a(t)&&this.#n.length>0&&await this.#c()}#d(){if(this.#n.length===0)return;let t=this.#n.slice(0,this.#t.batchSize),n;for(;t.length>0;){let r={batch_version:1,batch_id:this.#o.randomUUID(),project_key:this.#t.projectKey,sent_at:new Date(this.#o.now()).toISOString(),sdk:{name:"tracwell-browser",version:b},events:t};if(L(r)<=49152){n=r;break}t.pop()}if(!n){let r=this.#n.shift();r&&this.#t.onError?.({code:"EVENT_INVALID",eventIds:[r.event_id],message:"Event cannot fit in a valid browser batch."});return}return this.#n.splice(0,n.events.length),this.#e={attempt:0,batch:n},this.#e}#u(t){this.#t.onDelivery?.(t)}#p(t){let n=250*2**(t.attempt-1);this.#i=this.#o.setTimeout(()=>{this.#i=void 0,this.flush()},n)}async#a(t){let n;try{n=await this.#o.fetch(this.#t.endpoint,JSON.stringify(t.batch),"text/plain;charset=UTF-8")}catch(o){return this.#e!==t?!1:this.#s(t,o)}if(n.status!==202)return n.status===408||n.status===429||n.status>=500?this.#s(t,new Error(`Collector returned HTTP ${n.status}.`)):(this.#e===t&&(this.#e=void 0),this.#t.onError?.(f("DELIVERY_REJECTED",`Collector rejected the batch with non-retryable HTTP ${n.status}.`,t)),!1);let r;try{r=await n.json()}catch(o){return this.#t.onError?.(f("RECEIPT_INVALID","Collector returned an unreadable acceptance receipt.",t,o)),this.#s(t,o)}let i=z(r);return!i||i.batch_id!==t.batch.batch_id||i.accepted_events!==t.batch.events.length?(this.#t.onError?.(f("RECEIPT_INVALID","Collector receipt does not match the submitted batch.",t)),this.#s(t,new Error("Acceptance receipt mismatch."))):(this.#e===t&&(this.#e=void 0),this.#u({batchId:t.batch.batch_id,eventIds:t.batch.events.map(o=>o.event_id),receiptId:i.receipt_id,status:"accepted"}),!0)}#s(t,n){return this.#e!==t?!1:(t.attempt+=1,t.attempt<=this.#t.maxRetries?(this.#t.onError?.(f("DELIVERY_FAILED","Event batch delivery failed and is scheduled for bounded retry.",t,n)),this.#p(t),!1):(this.#e=void 0,this.#t.onError?.(f("DELIVERY_EXHAUSTED","Event batch delivery exhausted the configured retry limit.",t,n)),!1))}};function J(e,t){let n=new _(e,t),r=new y(e,t),i=t.consent,o,u=!1,p=!1,a=[];function T(){return i==="granted"&&!(t.respectDoNotTrack&&e.doNotTrackEnabled())}function S(s,c,d,h){if(p||!T())return;let V=G(e.page()),M=n.snapshot(V),l=h??M.userId,W=s==="identify"&&l?{eventName:"identify",eventType:"identify",...d?{properties:d}:{},userId:l}:s==="pageview"?{eventName:"page_view",eventType:"pageview",...d?{properties:d}:{},...l?{userId:l}:{}}:{eventName:c,eventType:"event",...d?{properties:d}:{},...l?{userId:l}:{}},v=j(e,M,W,t.onError);if(v)return s==="identify"&&h&&n.identify(h,V),r.enqueue(v),v.event_id}function g(s){if(!T())return;let c=new URL(e.page().href);c.hash="";let d=c.toString();if(d===o)return;let h=S("pageview","page_view",s);return h&&(o=d),h}function P(){a.forEach(s=>s()),a=[],u=!1}function x(){u||p||!T()||(u=!0,a=[e.addPageHideListener(()=>r.flushWithBeacon()),e.addVisibilityHiddenListener(()=>r.flushWithBeacon()),e.setInterval(()=>{r.flush()},t.flushIntervalMs)],t.autoTrackPageViews&&(a.push(e.subscribeToNavigation(()=>{g()})),g()))}return x(),{flush(){return r.flush()},identify(s,c){if(t.collectionMode==="private"){t.onError?.({code:"IDENTIFY_UNAVAILABLE",message:"identify() is unavailable in Private collection mode."});return}return S("identify","identify",c,s)},page:g,reset(){n.reset(),o=void 0},setConsent(s){if(i=s,s==="denied"){P(),r.clear(),n.reset(),o=void 0;return}x()},async shutdown(){p||(P(),await r.flush(),p=!0)},track(s,c){return S("event",s,c)}}}function $(e,t){return window.addEventListener(e,t),()=>window.removeEventListener(e,t)}function q(){return{addPageHideListener(e){return $("pagehide",e)},addVisibilityHiddenListener(e){let t=()=>{document.visibilityState==="hidden"&&e()};return document.addEventListener("visibilitychange",t),()=>document.removeEventListener("visibilitychange",t)},doNotTrackEnabled(){return navigator.doNotTrack==="1"},async fetch(e,t,n){return fetch(e,{body:t,credentials:"omit",headers:{"content-type":n},keepalive:!0,method:"POST"})},now(){return Date.now()},page(){return{href:window.location.href,referrer:document.referrer,title:document.title}},randomUUID(){return crypto.randomUUID()},sendBeacon(e,t){return navigator.sendBeacon(e,t)},setInterval(e,t){let n=window.setInterval(e,t);return()=>window.clearInterval(n)},setTimeout(e,t){let n=window.setTimeout(e,t);return()=>window.clearTimeout(n)},storage(){return window.localStorage},subscribeToNavigation(e){let t=!0,n=history.pushState,r=history.replaceState,i=()=>{t&&queueMicrotask(e)},o=function(...a){n.apply(history,a),i()},u=function(...a){r.apply(history,a),i()};history.pushState=o,history.replaceState=u;let p=$("popstate",e);return()=>{t=!1,p(),history.pushState===o&&(history.pushState=n),history.replaceState===u&&(history.replaceState=r)}}}}function Ft(e){if(typeof window>"u"||typeof document>"u"||typeof navigator>"u")throw new Error("createTracwell must be called in a browser after the document is available.");return J(q(),B(e))}export{k as DEFAULT_BATCH_SIZE,D as DEFAULT_COLLECTOR_ENDPOINT,U as DEFAULT_FLUSH_INTERVAL_MS,O as DEFAULT_MAX_RETRIES,H as DEFAULT_SESSION_TIMEOUT_MS,b as SDK_VERSION,Ft as createTracwell};
|
|
1
|
+
var k="https://collect.tracwell.app/v1/events",O=25,H=1e4,B=2,X=1800*1e3,b="0.2.0";function E(e,t){throw t?.({code:"CONFIG_INVALID",message:e}),new TypeError(e)}function et(e,t){let n=e.trim();return n.length<20||n.length>128||!/^[A-Za-z0-9_-]+$/u.test(n)?E("projectKey does not match the public project-key contract.",t):n}function nt(e,t){let n;try{n=new URL(e)}catch{return E("endpoint must be an absolute HTTPS URL.",t)}return rt(n,t)}function rt(e,t){let n=e.hostname==="localhost"||e.hostname==="127.0.0.1"||e.hostname==="[::1]";return e.protocol!=="https:"&&!n?E("endpoint must use HTTPS unless it uses a loopback host.",t):e.toString()}function y(e,t,n,r,i){return!Number.isSafeInteger(t)||t<n||t>r?E(`${e} must be an integer between ${n} and ${r}.`,i):t}function G(e){let t=et(e.projectKey,e.onError),n=e.collectionMode??"private",r=nt(e.endpoint??k,e.onError);return n==="private"&&e.persistence!==void 0&&e.persistence!=="none"?E('Private collection requires persistence: "none".',e.onError):{autoTrackPageViews:e.autoTrackPageViews??!0,batchSize:y("batchSize",e.batchSize??O,1,50,e.onError),consent:e.consent??"granted",collectionMode:n,endpoint:r,flushIntervalMs:y("flushIntervalMs",e.flushIntervalMs??H,1e3,6e4,e.onError),maxRetries:y("maxRetries",e.maxRetries??B,0,5,e.onError),persistence:n==="private"?"none":e.persistence??"localStorage",projectKey:t,respectDoNotTrack:e.respectDoNotTrack??!0,sessionTimeoutMs:y("sessionTimeoutMs",e.sessionTimeoutMs??X,6e4,1440*60*1e3,e.onError),...e.onDelivery?{onDelivery:e.onDelivery}:{},...e.onError?{onError:e.onError}:{}}}var Y=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"];function it(e){let t=e?.trim();return t||void 0}function ot(e){if(e)try{return new URL(e).toString()}catch{return}}function A(e){let t=new URL(e.href),n={},r=ot(e.referrer);r&&(n.referrer=r);for(let i of Y){let o=it(t.searchParams.get(i));o&&(n[i]=o)}return n}function R(e){let t=Y.map(n=>e[n]??"").join("");return t.replaceAll("","")?t:void 0}var w=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu,yt=/^[A-Za-z][A-Za-z0-9_.:-]*$/u,Tt=/[\s\x00-\x1f\x7f]/u,St=/[\x00-\x1f\x7f]/u;function C(e){return new TextEncoder().encode(JSON.stringify(e)).byteLength}function N(e){return e.length>0&&e.length<=256&&!Tt.test(e)}function K(e,t=2048){if(e.length>t)return!1;try{return new URL(e).origin.length>0}catch{return!1}}function L(e,t,n){return t>4?!1:typeof e=="string"?e.length<=1024:typeof e=="number"?Number.isFinite(e):e===null||typeof e=="boolean"?!0:Array.isArray(e)?e.length<=50&&e.every(r=>L(r,t+1,n)):Object.entries(e).every(([r,i])=>(n.count+=1,n.count<=100&&r.length>0&&r.length<=128&&!St.test(r)&&L(i,t+1,n)))}function z(e){let{context:t}=e;try{if(C(e)>16384)return!1}catch{return!1}return w.test(e.event_id)&&N(e.anonymous_id)&&N(e.session_id)&&(e.user_id===void 0||N(e.user_id))&&(e.event_type!=="event"||e.event_name.length<=100&&yt.test(e.event_name)&&e.event_name!=="identify"&&e.event_name!=="page_view")&&K(t.url)&&t.path.startsWith("/")&&t.path.length<=1024&&(t.title===void 0||t.title.length<=512)&&(t.referrer===void 0||K(t.referrer))&&[t.utm_source,t.utm_medium,t.utm_campaign,t.utm_term,t.utm_content].every(n=>n===void 0||n.length>0&&n.length<=256)&&(e.properties===void 0||L(e.properties,0,{count:0}))}function j(e){if(!e||typeof e!="object")return;let t=e;if(!(t.receipt_version!==1||typeof t.receipt_id!="string"||!w.test(t.receipt_id)||typeof t.batch_id!="string"||!w.test(t.batch_id)||!Number.isInteger(t.accepted_events)||(t.accepted_events??0)<1||(t.accepted_events??0)>50||typeof t.accepted_at!="string"||Number.isNaN(Date.parse(t.accepted_at))))return t}function It(e,t){let n=new URL(e.href);return n.hash="",{path:n.pathname,url:n.toString(),...e.title?{title:e.title}:{},...t}}function F(e,t,n,r){let i={schema_version:1,event_id:e.randomUUID(),event_name:n.eventName,event_type:n.eventType,timestamp:new Date(e.now()).toISOString(),anonymous_id:t.anonymousId,session_id:t.sessionId,context:It(e.page(),t.attribution),...n.properties?{properties:n.properties}:{},...n.userId?{user_id:n.userId}:{}};if(!z(i)){r?.({code:"EVENT_INVALID",message:"Event was not queued because it failed the v1 contract."});return}return i}var J=1;function gt(e){if(!e||typeof e!="object")return!1;let t=e;return t.version===J&&typeof t.anonymousId=="string"&&typeof t.sessionId=="string"&&typeof t.lastActivityAt=="number"&&typeof t.attribution=="object"&&t.attribution!==null&&(t.userId===void 0||typeof t.userId=="string")}var T=class{#e;#o;#t;#i;#r;#n;constructor(t,n){this.#t=t,this.#o=n.onError,this.#i=n.sessionTimeoutMs,this.#e=`tracwell:${n.projectKey}:identity`,this.#r=n.persistence==="localStorage"?this.#p(t):void 0}identify(t,n){let r=this.#d(n);return r.userId&&r.userId!==t&&(r.anonymousId=this.#t.randomUUID(),r.sessionId=this.#t.randomUUID()),r.userId=t,this.#h(r),this.#s(r)}reset(){if(this.#n=void 0,this.#r)try{this.#r.removeItem(this.#e)}catch(t){this.#a(t)}}snapshot(t){return this.#s(this.#d(t))}#c(t){let n=this.#t.now();return{anonymousId:this.#t.randomUUID(),attribution:t,lastActivityAt:n,sessionId:this.#t.randomUUID(),version:J}}#d(t){let n=this.#t.now(),r=this.#u();if(!r){let a=this.#c(t);return this.#h(a),a}let i=R(r.attribution),o=R(t);return(n-r.lastActivityAt>=this.#i||o!==void 0&&o!==i)&&(r.sessionId=this.#t.randomUUID(),r.attribution=t),r.lastActivityAt=n,this.#h(r),r}#u(){if(this.#n)return this.#n;if(this.#r)try{let t=this.#r.getItem(this.#e);if(!t)return;let n=JSON.parse(t);if(!gt(n)){this.#r.removeItem(this.#e);return}return this.#n=n,n}catch(t){this.#a(t);return}}#p(t){try{return t.storage()}catch(n){this.#a(n);return}}#a(t){this.#o?.({cause:t,code:"STORAGE_UNAVAILABLE",message:"Persistent identity storage is unavailable; using in-memory identity."})}#s(t){return{anonymousId:t.anonymousId,attribution:t.attribution,sessionId:t.sessionId,...t.userId?{userId:t.userId}:{}}}#h(t){if(this.#n=t,!!this.#r)try{this.#r.setItem(this.#e,JSON.stringify(t))}catch(n){this.#a(n)}}};function f(e,t,n,r){return{...r===void 0?{}:{cause:r},code:e,eventIds:n.batch.events.map(i=>i.event_id),message:t}}var S=class{#e;#o;#t;#i;#r;#n=[];constructor(t,n){this.#o=t,this.#e=n}enqueue(t){this.#n.push(t),this.#n.length>=this.#e.batchSize&&this.flush()}clear(){this.#i?.(),this.#i=void 0,this.#t=void 0,this.#n=[]}flush(){return this.#r?this.#r:(this.#r=this.#c().finally(()=>{this.#r=void 0}),this.#r)}flushWithBeacon(){for(this.#i?.(),this.#i=void 0;this.#t||this.#n.length>0;){let t=this.#t??this.#d();if(!t)return;let n=JSON.stringify(t.batch);if(!this.#o.sendBeacon(this.#e.endpoint,n)){this.#e.onError?.(f("DELIVERY_FAILED","Browser lifecycle handoff was rejected by sendBeacon.",t));return}this.#t=void 0,this.#u({batchId:t.batch.batch_id,eventIds:t.batch.events.map(r=>r.event_id),status:"handed_off"})}}async#c(){this.#i?.(),this.#i=void 0;let t=this.#t??this.#d();if(!t)return;await this.#a(t)&&this.#n.length>0&&await this.#c()}#d(){if(this.#n.length===0)return;let t=this.#n.slice(0,this.#e.batchSize),n;for(;t.length>0;){let r={batch_version:1,batch_id:this.#o.randomUUID(),project_key:this.#e.projectKey,sent_at:new Date(this.#o.now()).toISOString(),sdk:{name:"tracwell-browser",version:b},events:t};if(C(r)<=49152){n=r;break}t.pop()}if(!n){let r=this.#n.shift();r&&this.#e.onError?.({code:"EVENT_INVALID",eventIds:[r.event_id],message:"Event cannot fit in a valid browser batch."});return}return this.#n.splice(0,n.events.length),this.#t={attempt:0,batch:n},this.#t}#u(t){this.#e.onDelivery?.(t)}#p(t){let n=250*2**(t.attempt-1);this.#i=this.#o.setTimeout(()=>{this.#i=void 0,this.flush()},n)}async#a(t){let n;try{n=await this.#o.fetch(this.#e.endpoint,JSON.stringify(t.batch),"text/plain;charset=UTF-8")}catch(o){return this.#t!==t?!1:this.#s(t,o)}if(n.status!==202)return n.status===408||n.status===429||n.status>=500?this.#s(t,new Error(`Collector returned HTTP ${n.status}.`)):(this.#t===t&&(this.#t=void 0),this.#e.onError?.(f("DELIVERY_REJECTED",`Collector rejected the batch with non-retryable HTTP ${n.status}.`,t)),!1);let r;try{r=await n.json()}catch(o){return this.#e.onError?.(f("RECEIPT_INVALID","Collector returned an unreadable acceptance receipt.",t,o)),this.#s(t,o)}let i=j(r);return!i||i.batch_id!==t.batch.batch_id||i.accepted_events!==t.batch.events.length?(this.#e.onError?.(f("RECEIPT_INVALID","Collector receipt does not match the submitted batch.",t)),this.#s(t,new Error("Acceptance receipt mismatch."))):(this.#t===t&&(this.#t=void 0),this.#u({batchId:t.batch.batch_id,eventIds:t.batch.events.map(o=>o.event_id),receiptId:i.receipt_id,status:"accepted"}),!0)}#s(t,n){return this.#t!==t?!1:(t.attempt+=1,t.attempt<=this.#e.maxRetries?(this.#e.onError?.(f("DELIVERY_FAILED","Event batch delivery failed and is scheduled for bounded retry.",t,n)),this.#p(t),!1):(this.#t=void 0,this.#e.onError?.(f("DELIVERY_EXHAUSTED","Event batch delivery exhausted the configured retry limit.",t,n)),!1))}};function $(e,t){let n=new T(e,t),r=new S(e,t),i=t.consent,o,p=!1,c=!1,a=[];function _(){return i==="granted"&&!(t.respectDoNotTrack&&e.doNotTrackEnabled())}function I(s,u,d,h){if(c||!_())return;let V=A(e.page()),D=n.snapshot(V),l=h??D.userId,Q=s==="identify"&&l?{eventName:"identify",eventType:"identify",...d?{properties:d}:{},userId:l}:s==="pageview"?{eventName:"page_view",eventType:"pageview",...d?{properties:d}:{},...l?{userId:l}:{}}:{eventName:u,eventType:"event",...d?{properties:d}:{},...l?{userId:l}:{}},m=F(e,D,Q,t.onError);if(m){if(s==="identify"&&h){let U=n.identify(h,V);m.anonymous_id=U.anonymousId,m.session_id=U.sessionId}return r.enqueue(m),m.event_id}}function g(s){if(!_())return;let u=new URL(e.page().href);u.hash="";let d=u.toString();if(d===o)return;let h=I("pageview","page_view",s);return h&&(o=d),h}function x(){a.forEach(s=>s()),a=[],p=!1}function M(){p||c||!_()||(p=!0,a=[e.addPageHideListener(()=>r.flushWithBeacon()),e.addVisibilityHiddenListener(()=>r.flushWithBeacon()),e.setInterval(()=>{r.flush()},t.flushIntervalMs)],t.autoTrackPageViews&&(a.push(e.subscribeToNavigation(()=>{g()})),g()))}return M(),{flush(){return r.flush()},getSession(){if(c||t.collectionMode!=="product"||!_())return;let s=n.snapshot(A(e.page()));return{anonymousId:s.anonymousId,sessionId:s.sessionId,...s.userId?{userId:s.userId}:{}}},identify(s,u){if(t.collectionMode==="private"){t.onError?.({code:"IDENTIFY_UNAVAILABLE",message:"identify() is unavailable in Private collection mode."});return}return I("identify","identify",u,s)},page:g,reset(){n.reset(),o=void 0},setConsent(s){if(i=s,s==="denied"){x(),r.clear(),n.reset(),o=void 0;return}M()},async shutdown(){c||(x(),await r.flush(),c=!0)},track(s,u){return I("event",s,u)}}}function q(e,t){return window.addEventListener(e,t),()=>window.removeEventListener(e,t)}function W(){return{addPageHideListener(e){return q("pagehide",e)},addVisibilityHiddenListener(e){let t=()=>{document.visibilityState==="hidden"&&e()};return document.addEventListener("visibilitychange",t),()=>document.removeEventListener("visibilitychange",t)},doNotTrackEnabled(){return navigator.doNotTrack==="1"},async fetch(e,t,n){return fetch(e,{body:t,credentials:"omit",headers:{"content-type":n},keepalive:!0,method:"POST"})},now(){return Date.now()},page(){return{href:window.location.href,referrer:document.referrer,title:document.title}},randomUUID(){return crypto.randomUUID()},sendBeacon(e,t){return navigator.sendBeacon(e,t)},setInterval(e,t){let n=window.setInterval(e,t);return()=>window.clearInterval(n)},setTimeout(e,t){let n=window.setTimeout(e,t);return()=>window.clearTimeout(n)},storage(){return window.localStorage},subscribeToNavigation(e){let t=!0,n=history.pushState,r=history.replaceState,i=()=>{t&&queueMicrotask(e)},o=function(...a){n.apply(history,a),i()},p=function(...a){r.apply(history,a),i()};history.pushState=o,history.replaceState=p;let c=q("popstate",e);return()=>{t=!1,c(),history.pushState===o&&(history.pushState=n),history.replaceState===p&&(history.replaceState=r)}}}}function Jt(e){if(typeof window>"u"||typeof document>"u"||typeof navigator>"u")throw new Error("createTracwell must be called in a browser after the document is available.");return $(W(),G(e))}export{O as DEFAULT_BATCH_SIZE,k as DEFAULT_COLLECTOR_ENDPOINT,H as DEFAULT_FLUSH_INTERVAL_MS,B as DEFAULT_MAX_RETRIES,X as DEFAULT_SESSION_TIMEOUT_MS,b as SDK_VERSION,Jt as createTracwell};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface CrawlerTrackingOptions {
|
|
2
|
+
/** Secret tw_sk_ key. Never include in browser code. */
|
|
3
|
+
serverKey: string;
|
|
4
|
+
collectorUrl?: string;
|
|
5
|
+
statusCode?: number;
|
|
6
|
+
}
|
|
7
|
+
export type CrawlerTrackingResult = {
|
|
8
|
+
status: "ignored";
|
|
9
|
+
} | {
|
|
10
|
+
status: "accepted";
|
|
11
|
+
eventId: string;
|
|
12
|
+
} | {
|
|
13
|
+
status: "failed";
|
|
14
|
+
eventId: string;
|
|
15
|
+
code: "CONFIGURATION" | "DELIVERY_REJECTED" | "DELIVERY_UNAVAILABLE";
|
|
16
|
+
};
|
|
17
|
+
/** Await on a persistent server, or pass the promise to the runtime's waitUntil. */
|
|
18
|
+
export declare function trackAiCrawler(request: Request, options: CrawlerTrackingOptions): Promise<CrawlerTrackingResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var l=[{token:"ChatGPT-User",provider:"OpenAI",category:"answers"},{token:"OAI-SearchBot",provider:"OpenAI",category:"indexing"},{token:"GPTBot",provider:"OpenAI",category:"training"},{token:"Claude-User",provider:"Anthropic",category:"answers"},{token:"Claude-SearchBot",provider:"Anthropic",category:"indexing"},{token:"ClaudeBot",provider:"Anthropic",category:"training"},{token:"Perplexity-User",provider:"Perplexity",category:"answers"},{token:"PerplexityBot",provider:"Perplexity",category:"indexing"}];function p(n){return l.find(r=>new RegExp(`(?:^|[\\s;(])${r.token}(?:/|[\\s;)]|$)`,"i").test(n))}async function h(n,r){let c=n.headers.get("user-agent")??"";if(!["GET","HEAD"].includes(n.method)||!p(c))return{status:"ignored"};let o=new URL(n.url);if(/\.(?:js|css|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|mp4|map)$/i.test(o.pathname)||/^\/(?:api|_next)(?:\/|$)/.test(o.pathname))return{status:"ignored"};let a=crypto.randomUUID(),e;try{if(e=new URL(r.collectorUrl??"https://collect.tracwell.app/v1/server/crawlers"),e.username||e.password||e.search||e.hash||e.protocol!=="https:"&&!(e.protocol==="http:"&&["localhost","127.0.0.1","[::1]"].includes(e.hostname))||!/^tw_sk_[A-Za-z0-9_-]+$/.test(r.serverKey))throw new Error("Invalid configuration")}catch{return{status:"failed",eventId:a,code:"CONFIGURATION"}}o.search="",o.hash="",o.username="",o.password="";let d=JSON.stringify({schema_version:1,event_id:a,timestamp:new Date().toISOString(),url:o.href,user_agent:c.slice(0,1024),...r.statusCode===void 0?{}:{status_code:r.statusCode}});for(let i=0;i<3;i++){try{let s=await fetch(e,{method:"POST",redirect:"error",signal:AbortSignal.timeout(2e3),headers:{"content-type":"application/json",authorization:`Bearer ${r.serverKey}`},body:d});if(s.status===202){let t=await s.json();return t&&typeof t=="object"&&"batch_id"in t&&t.batch_id===a&&"accepted_events"in t&&t.accepted_events===1&&"receipt_version"in t&&t.receipt_version===1?{status:"accepted",eventId:a}:{status:"failed",eventId:a,code:"DELIVERY_REJECTED"}}if(s.status!==408&&s.status!==429&&s.status<500)return{status:"failed",eventId:a,code:"DELIVERY_REJECTED"}}catch{}i<2&&await new Promise(s=>setTimeout(s,150*(i+1)))}return{status:"failed",eventId:a,code:"DELIVERY_UNAVAILABLE"}}export{h as trackAiCrawler};
|
package/dist/npm/types.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface DeliveryReport {
|
|
|
25
25
|
status: "accepted" | "handed_off";
|
|
26
26
|
receiptId?: string;
|
|
27
27
|
}
|
|
28
|
+
export interface TracwellSession {
|
|
29
|
+
anonymousId: string;
|
|
30
|
+
sessionId: string;
|
|
31
|
+
userId?: string;
|
|
32
|
+
}
|
|
28
33
|
export interface TracwellConfig {
|
|
29
34
|
projectKey: string;
|
|
30
35
|
collectionMode?: CollectionMode;
|
|
@@ -42,6 +47,7 @@ export interface TracwellConfig {
|
|
|
42
47
|
}
|
|
43
48
|
export interface TracwellClient {
|
|
44
49
|
flush(): Promise<void>;
|
|
50
|
+
getSession(): TracwellSession | undefined;
|
|
45
51
|
identify(userId: string, properties?: EventProperties): string | undefined;
|
|
46
52
|
page(properties?: EventProperties): string | undefined;
|
|
47
53
|
reset(): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tracwell",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Privacy-aware browser analytics and optional server-side AI crawler tracking for Tracwell",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
"types": "./dist/npm/index.d.ts",
|
|
14
14
|
"import": "./dist/npm/index.js",
|
|
15
15
|
"default": "./dist/npm/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./server": {
|
|
18
|
+
"types": "./dist/npm/server.d.ts",
|
|
19
|
+
"import": "./dist/npm/server.js",
|
|
20
|
+
"default": "./dist/npm/server.js"
|
|
16
21
|
}
|
|
17
22
|
},
|
|
18
23
|
"files": [
|
|
@@ -21,7 +26,9 @@
|
|
|
21
26
|
"dist/npm/config.d.ts",
|
|
22
27
|
"dist/npm/types.d.ts",
|
|
23
28
|
"LICENSE",
|
|
24
|
-
"README.md"
|
|
29
|
+
"README.md",
|
|
30
|
+
"dist/npm/server.js",
|
|
31
|
+
"dist/npm/server.d.ts"
|
|
25
32
|
],
|
|
26
33
|
"keywords": [
|
|
27
34
|
"analytics",
|