apptvty 0.1.2
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 +537 -0
- package/dist/chunk-RGUS6IL6.mjs +87 -0
- package/dist/chunk-RGUS6IL6.mjs.map +1 -0
- package/dist/chunk-WATTAPBA.mjs +502 -0
- package/dist/chunk-WATTAPBA.mjs.map +1 -0
- package/dist/chunk-XOWRKLFM.mjs +150 -0
- package/dist/chunk-XOWRKLFM.mjs.map +1 -0
- package/dist/cli.js +321 -0
- package/dist/index.d.mts +170 -0
- package/dist/index.d.ts +170 -0
- package/dist/index.js +751 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +29 -0
- package/dist/index.mjs.map +1 -0
- package/dist/middleware/express.d.mts +46 -0
- package/dist/middleware/express.d.ts +46 -0
- package/dist/middleware/express.js +595 -0
- package/dist/middleware/express.js.map +1 -0
- package/dist/middleware/express.mjs +10 -0
- package/dist/middleware/express.mjs.map +1 -0
- package/dist/middleware/nextjs.d.mts +47 -0
- package/dist/middleware/nextjs.d.ts +47 -0
- package/dist/middleware/nextjs.js +658 -0
- package/dist/middleware/nextjs.js.map +1 -0
- package/dist/middleware/nextjs.mjs +10 -0
- package/dist/middleware/nextjs.mjs.map +1 -0
- package/dist/setup.d.mts +71 -0
- package/dist/setup.d.ts +71 -0
- package/dist/setup.js +110 -0
- package/dist/setup.js.map +1 -0
- package/dist/setup.mjs +82 -0
- package/dist/setup.mjs.map +1 -0
- package/dist/types-C1oUTCsT.d.mts +263 -0
- package/dist/types-C1oUTCsT.d.ts +263 -0
- package/package.json +82 -0
package/README.md
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
# apptvty
|
|
2
|
+
|
|
3
|
+
**AI traffic analytics and Agent Experience Optimization (AEO) for Node.js websites.**
|
|
4
|
+
|
|
5
|
+
Apptvty makes your website visible to, and queryable by, AI agents — the crawlers and language models (GPTBot, ClaudeBot, Perplexity, etc.) that are increasingly the primary consumers of web content.
|
|
6
|
+
|
|
7
|
+
- **Detects and classifies AI traffic** that standard analytics tools miss entirely
|
|
8
|
+
- **Exposes a `/query` endpoint** so AI agents can get structured, sourced answers from your site
|
|
9
|
+
- **Earns USDC** for your site when sponsored ads are served in query responses
|
|
10
|
+
- **Two lines to integrate** into an existing Next.js or Express app
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
npm install apptvty
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Node.js >= 18 required.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
### Next.js (App Router)
|
|
23
|
+
|
|
24
|
+
**Step 1 — Log all traffic**
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// middleware.ts
|
|
28
|
+
import { withApptvty } from 'apptvty/nextjs';
|
|
29
|
+
|
|
30
|
+
export default withApptvty({
|
|
31
|
+
apiKey: process.env.APPTVTY_API_KEY!,
|
|
32
|
+
siteId: process.env.APPTVTY_SITE_ID!,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const config = {
|
|
36
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
37
|
+
};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Step 2 — Expose the query endpoint**
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// app/query/route.ts
|
|
44
|
+
import { createNextjsQueryHandler } from 'apptvty/nextjs';
|
|
45
|
+
|
|
46
|
+
export const GET = createNextjsQueryHandler({
|
|
47
|
+
apiKey: process.env.APPTVTY_API_KEY!,
|
|
48
|
+
siteId: process.env.APPTVTY_SITE_ID!,
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**That's it.** Every request is now logged, AI crawlers are classified, and AI agents can query your site at `https://yoursite.com/query?q=your+question`.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
### Express
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import express from 'express';
|
|
60
|
+
import { createExpressMiddleware, createExpressQueryHandler } from 'apptvty/express';
|
|
61
|
+
|
|
62
|
+
const app = express();
|
|
63
|
+
const config = {
|
|
64
|
+
apiKey: process.env.APPTVTY_API_KEY!,
|
|
65
|
+
siteId: process.env.APPTVTY_SITE_ID!,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Log all requests
|
|
69
|
+
app.use(createExpressMiddleware(config));
|
|
70
|
+
|
|
71
|
+
// Expose the query endpoint
|
|
72
|
+
app.get('/query', createExpressQueryHandler(config));
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### CLI setup (humans and agents)
|
|
78
|
+
|
|
79
|
+
The fastest way to get credentials and scaffold files:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Interactive (human)
|
|
83
|
+
npx apptvty init
|
|
84
|
+
|
|
85
|
+
# Non-interactive (agent / CI) — prints JSON to stdout
|
|
86
|
+
npx apptvty init --domain mysite.com --framework nextjs --non-interactive
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Trigger a re-crawl after publishing new content:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Reindex your site (reads APPTVTY_SITE_ID and APPTVTY_API_KEY from .env.local / .env)
|
|
93
|
+
npx apptvty migrate
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
CLI commands:
|
|
97
|
+
|
|
98
|
+
| Command | Description |
|
|
99
|
+
|---------|-------------|
|
|
100
|
+
| `init` | Register site and scaffold integration files (default) |
|
|
101
|
+
| `migrate` | Trigger immediate re-crawl/reindex of your site |
|
|
102
|
+
|
|
103
|
+
CLI flags:
|
|
104
|
+
|
|
105
|
+
| Flag | Description |
|
|
106
|
+
|------|-------------|
|
|
107
|
+
| `--domain` | Your site's domain, e.g. `mysite.com` (init only) |
|
|
108
|
+
| `--framework` | `nextjs`, `express`, or `other` (init only, auto-detected) |
|
|
109
|
+
| `--non-interactive` | No prompts; JSON output for scripting/agents |
|
|
110
|
+
| `--api-url` | Override API base URL (staging / self-hosted) |
|
|
111
|
+
|
|
112
|
+
**Agent-mode JSON output:**
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"site_id": "site_abc123",
|
|
116
|
+
"api_key": "ak_live_...",
|
|
117
|
+
"company_id": "co_xyz...",
|
|
118
|
+
"wallet_address": "0x...",
|
|
119
|
+
"dashboard_url": "https://app.apptvty.com/claim?token=...",
|
|
120
|
+
"env_file": ".env.local",
|
|
121
|
+
"env_vars": {
|
|
122
|
+
"APPTVTY_SITE_ID": "site_abc123",
|
|
123
|
+
"APPTVTY_API_KEY": "ak_live_..."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The CLI writes credentials to `.env.local` (Next.js) or `.env` (Express/other) and scaffolds `middleware.ts` and `app/query/route.ts` if they do not already exist.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Package exports
|
|
133
|
+
|
|
134
|
+
| Import | Contents |
|
|
135
|
+
|--------|----------|
|
|
136
|
+
| `apptvty` | Core client, logger, crawler detector, query handler (framework-agnostic) |
|
|
137
|
+
| `apptvty/nextjs` | Next.js App Router middleware and route handler |
|
|
138
|
+
| `apptvty/express` | Express/Connect middleware and route handler |
|
|
139
|
+
| `apptvty/setup` | `register()` and `migrate()` for programmatic setup and reindex |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Configuration
|
|
144
|
+
|
|
145
|
+
All middleware and handler functions accept an `ApptvtyConfig` object:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
interface ApptvtyConfig {
|
|
149
|
+
apiKey: string; // Required. Your site's API key (ak_...)
|
|
150
|
+
siteId: string; // Required. Your site ID from the dashboard
|
|
151
|
+
baseUrl?: string; // Optional. Uses APPTVTY_API_URL env if set.
|
|
152
|
+
batchSize?: number; // Default: 50 — logs per flush
|
|
153
|
+
flushInterval?: number; // Default: 5000 — ms between auto-flushes
|
|
154
|
+
debug?: boolean; // Default: false — log errors to console
|
|
155
|
+
queryPath?: string; // Default: '/query' — path of AEO endpoint
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Set these from environment variables so credentials are never in source code:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
APPTVTY_API_KEY=ak_live_...
|
|
163
|
+
APPTVTY_SITE_ID=site_...
|
|
164
|
+
APPTVTY_API_URL=https://api.apptvty.com # Optional. Default is api.apptvty.com; override for self-hosted only.
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
See [.env.example](.env.example) for a copy-paste template.
|
|
168
|
+
|
|
169
|
+
**Production:** The SDK uses **https://api.apptvty.com** by default. You do not need to set `APPTVTY_API_URL` for production.
|
|
170
|
+
|
|
171
|
+
### Using the SDK with a self-hosted backend
|
|
172
|
+
|
|
173
|
+
Only if you run **apptvty-backend** yourself (e.g. Serverless in your AWS account), point the SDK at that API:
|
|
174
|
+
|
|
175
|
+
1. **Deploy the backend** (from the backend repo):
|
|
176
|
+
```bash
|
|
177
|
+
cd apptvty-backend
|
|
178
|
+
npx sls deploy --stage dev
|
|
179
|
+
```
|
|
180
|
+
2. **Get the API URL** from the stack output:
|
|
181
|
+
```bash
|
|
182
|
+
npx sls info --stage dev
|
|
183
|
+
```
|
|
184
|
+
Use the **ApiGatewayRestApiUrl** (e.g. `https://xxxx.execute-api.us-west-2.amazonaws.com/dev`).
|
|
185
|
+
3. **Override the default (api.apptvty.com)** in one of two ways:
|
|
186
|
+
- **Environment:** Set `APPTVTY_API_URL` to that URL (no trailing slash).
|
|
187
|
+
- **In code:** Pass `baseUrl` (or `apiUrl` for `register`/`migrate`).
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## The query endpoint
|
|
192
|
+
|
|
193
|
+
When an AI agent visits `https://yoursite.com/query`, the SDK responds with a self-describing discovery page so the agent knows how to use it:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"version": "1.0",
|
|
198
|
+
"endpoint": "https://yoursite.com/query",
|
|
199
|
+
"description": "Query this site's content using natural language.",
|
|
200
|
+
"usage": {
|
|
201
|
+
"method": "GET",
|
|
202
|
+
"parameters": {
|
|
203
|
+
"q": { "type": "string", "required": true, "description": "Natural language question" },
|
|
204
|
+
"lang": { "type": "string", "required": false, "description": "Preferred language (ISO 639-1)" }
|
|
205
|
+
},
|
|
206
|
+
"example": "https://yoursite.com/query?q=What+does+this+site+do"
|
|
207
|
+
},
|
|
208
|
+
"capabilities": ["rag", "sponsored_ads"],
|
|
209
|
+
"rate_limit": "60 requests per minute"
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
When called with `?q=<question>`:
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"success": true,
|
|
218
|
+
"version": "1.0",
|
|
219
|
+
"query": "What does this site do?",
|
|
220
|
+
"answer": "Apptvty is an AI traffic analytics platform ...",
|
|
221
|
+
"sources": [
|
|
222
|
+
{
|
|
223
|
+
"url": "https://yoursite.com/about",
|
|
224
|
+
"title": "About Apptvty",
|
|
225
|
+
"snippet": "Apptvty logs and classifies AI crawler traffic ...",
|
|
226
|
+
"relevance": 0.94
|
|
227
|
+
}
|
|
228
|
+
],
|
|
229
|
+
"confidence": 0.91,
|
|
230
|
+
"sponsored": {
|
|
231
|
+
"label": "Sponsored",
|
|
232
|
+
"text": "Get deeper AI analytics with AEO Pro — 14-day free trial",
|
|
233
|
+
"url": "https://...",
|
|
234
|
+
"advertiser": "AEO Pro",
|
|
235
|
+
"impression_id": "imp_..."
|
|
236
|
+
},
|
|
237
|
+
"metadata": {
|
|
238
|
+
"request_id": "req_...",
|
|
239
|
+
"response_time_ms": 340,
|
|
240
|
+
"tokens_used": 420,
|
|
241
|
+
"site_id": "site_...",
|
|
242
|
+
"timestamp": "2026-03-06T12:00:00.000Z"
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The `sponsored` field is present only when ads are enabled for your site and a matching ad exists. Ads are clearly labeled and serve as a revenue stream for site owners — no action required by the agent beyond passing the field through to the end user.
|
|
248
|
+
|
|
249
|
+
**Error response:**
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"success": false,
|
|
253
|
+
"error": {
|
|
254
|
+
"code": "QUERY_TOO_LONG",
|
|
255
|
+
"message": "Query exceeds the 500-character limit.",
|
|
256
|
+
"request_id": "req_...",
|
|
257
|
+
"timestamp": "2026-03-06T12:00:00.000Z"
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Response headers always include:
|
|
263
|
+
- `Content-Type: application/json`
|
|
264
|
+
- `Cache-Control: no-store`
|
|
265
|
+
- `X-Robots-Tag: noindex`
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Analytics for coding agents
|
|
270
|
+
|
|
271
|
+
The SDK exposes methods so coding agents (e.g. Cursor, Claude Code) can fetch logs, activity, and errors without human intervention — similar to how `aws logs tail` works for AWS CLI.
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { ApptvtyClient } from 'apptvty';
|
|
275
|
+
|
|
276
|
+
const client = new ApptvtyClient({
|
|
277
|
+
apiKey: process.env.APPTVTY_API_KEY!,
|
|
278
|
+
siteId: process.env.APPTVTY_SITE_ID!,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// 30-day overview
|
|
282
|
+
const stats = await client.getSiteStats();
|
|
283
|
+
console.log(`AI traffic: ${stats.ai_percentage}%`);
|
|
284
|
+
|
|
285
|
+
// Recent activity (last hour)
|
|
286
|
+
const { activity } = await client.getSiteActivity(20);
|
|
287
|
+
activity.forEach(a => console.log(a.path, a.crawler_type, a.status_code));
|
|
288
|
+
|
|
289
|
+
// Recent agent queries
|
|
290
|
+
const { queries } = await client.getSiteQueries(10);
|
|
291
|
+
|
|
292
|
+
// Crawler breakdown
|
|
293
|
+
const { crawlers } = await client.getSiteCrawlers(7);
|
|
294
|
+
|
|
295
|
+
// Daily stats
|
|
296
|
+
const { stats: daily } = await client.getSiteDailyStats(30);
|
|
297
|
+
|
|
298
|
+
// Wallet balance
|
|
299
|
+
const wallet = await client.getSiteWallet();
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
| Method | Returns |
|
|
303
|
+
|--------|---------|
|
|
304
|
+
| `getSiteStats()` | 30-day overview (requests, AI %, crawlers, queries) |
|
|
305
|
+
| `getSiteDailyStats(days?)` | Per-day breakdown |
|
|
306
|
+
| `getSiteActivity(limit?)` | Recent requests (path, crawler, status) |
|
|
307
|
+
| `getSiteQueries(limit?)` | Recent agent queries |
|
|
308
|
+
| `getSiteCrawlers(days?)` | Crawler type breakdown |
|
|
309
|
+
| `getSiteWallet()` | Balance, earnings, spend |
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Crawler detection
|
|
314
|
+
|
|
315
|
+
The SDK identifies 50+ AI crawlers by user-agent string. Access detection directly:
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { detectCrawler, getKnownCrawlerNames } from 'apptvty';
|
|
319
|
+
|
|
320
|
+
const info = detectCrawler('GPTBot/1.1');
|
|
321
|
+
// {
|
|
322
|
+
// isAi: true,
|
|
323
|
+
// name: 'GPTBot',
|
|
324
|
+
// organization: 'OpenAI',
|
|
325
|
+
// confidence: 0.95,
|
|
326
|
+
// detectionMethod: 'exact_match'
|
|
327
|
+
// }
|
|
328
|
+
|
|
329
|
+
const names = getKnownCrawlerNames();
|
|
330
|
+
// ['GPTBot', 'ClaudeBot', 'PerplexityBot', 'BingBot', ...]
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**`CrawlerInfo` fields:**
|
|
334
|
+
|
|
335
|
+
| Field | Type | Description |
|
|
336
|
+
|-------|------|-------------|
|
|
337
|
+
| `isAi` | `boolean` | Whether the traffic is from an AI/LLM system |
|
|
338
|
+
| `name` | `string \| null` | Crawler name, e.g. `"GPTBot"` |
|
|
339
|
+
| `organization` | `string \| null` | Organization, e.g. `"OpenAI"` |
|
|
340
|
+
| `confidence` | `number` | Detection certainty, 0.0 – 1.0 |
|
|
341
|
+
| `detectionMethod` | `string` | `exact_match` / `pattern_match` / `heuristic` / `none` |
|
|
342
|
+
|
|
343
|
+
**Crawlers recognized (partial list):** GPTBot, OpenAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-Web, Anthropic-AI, Google-Extended, GoogleOther, Bingbot, PerplexityBot, YouBot, DuckDuckBot, Meta-ExternalAgent, LinkedInBot, AppleBot, TwitterBot, CohereAI, AI2Bot, MistralBot.
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Programmatic registration
|
|
348
|
+
|
|
349
|
+
For agents and scripts that need to register a site without the CLI:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import { register, RegistrationError } from 'apptvty/setup';
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const result = await register({
|
|
356
|
+
domain: 'mysite.com',
|
|
357
|
+
framework: 'nextjs', // 'nextjs' | 'express' | 'other'
|
|
358
|
+
agentId: 'my-agent', // optional — analytics on which agent registered
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
console.log(result.apiKey); // 'ak_live_...'
|
|
362
|
+
console.log(result.siteId); // 'site_...'
|
|
363
|
+
console.log(result.walletAddress); // '0x...' or null
|
|
364
|
+
console.log(result.dashboardUrl); // Dashboard claim link (valid 30 days)
|
|
365
|
+
console.log(result.setup.envVars); // { APPTVTY_SITE_ID, APPTVTY_API_KEY }
|
|
366
|
+
|
|
367
|
+
} catch (err) {
|
|
368
|
+
if (err instanceof RegistrationError) {
|
|
369
|
+
// err.code: 'DOMAIN_TAKEN' | 'REGISTRATION_FAILED' | ...
|
|
370
|
+
console.error(err.code, err.message);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**`RegisterOptions`:**
|
|
376
|
+
|
|
377
|
+
| Field | Type | Required | Description |
|
|
378
|
+
|-------|------|----------|-------------|
|
|
379
|
+
| `domain` | `string` | Yes | Site domain, e.g. `mysite.com` |
|
|
380
|
+
| `framework` | `string` | No | `nextjs`, `express`, or `other` |
|
|
381
|
+
| `agentId` | `string` | No | Identifies the registering agent (for analytics) |
|
|
382
|
+
| `apiUrl` | `string` | No | Override API base URL for staging/self-hosted |
|
|
383
|
+
|
|
384
|
+
**`RegisterResult`:**
|
|
385
|
+
|
|
386
|
+
| Field | Type | Description |
|
|
387
|
+
|-------|------|-------------|
|
|
388
|
+
| `siteId` | `string` | Site identifier |
|
|
389
|
+
| `apiKey` | `string` | API key for SDK config |
|
|
390
|
+
| `companyId` | `string` | Company identifier |
|
|
391
|
+
| `walletAddress` | `string \| null` | Crossmint wallet for USDC earnings |
|
|
392
|
+
| `dashboardUrl` | `string` | Dashboard claim link (valid 30 days) |
|
|
393
|
+
| `claimTokenExpiresAt` | `string` | ISO timestamp of link expiry |
|
|
394
|
+
| `setup.envVars` | `object` | Env vars to write to your `.env` file |
|
|
395
|
+
| `setup.files` | `object \| undefined` | Optional scaffold file contents |
|
|
396
|
+
|
|
397
|
+
Registration is idempotent per domain. Registering the same domain twice throws `RegistrationError` with `code: 'DOMAIN_TAKEN'`.
|
|
398
|
+
|
|
399
|
+
### Programmatic migrate (reindex)
|
|
400
|
+
|
|
401
|
+
Trigger an immediate re-crawl after publishing new content:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
import { migrate, MigrateError } from 'apptvty/setup';
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const result = await migrate({
|
|
408
|
+
siteId: process.env.APPTVTY_SITE_ID!,
|
|
409
|
+
apiKey: process.env.APPTVTY_API_KEY!,
|
|
410
|
+
apiUrl: 'https://api.apptvty.com', // optional — for staging/self-hosted
|
|
411
|
+
});
|
|
412
|
+
console.log(result.message); // "Reindex started. Your site content will be updated within a few minutes."
|
|
413
|
+
} catch (err) {
|
|
414
|
+
if (err instanceof MigrateError) {
|
|
415
|
+
console.error(err.code, err.message);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
The crawl runs asynchronously; the endpoint returns 202 immediately.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Advanced: framework-agnostic usage
|
|
425
|
+
|
|
426
|
+
Use `ApptvtyClient` and `RequestLogger` directly for custom frameworks:
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
import { ApptvtyClient, RequestLogger, detectCrawler, createQueryHandler } from 'apptvty';
|
|
430
|
+
|
|
431
|
+
const config = { apiKey: 'ak_...', siteId: 'site_...' };
|
|
432
|
+
const client = new ApptvtyClient(config);
|
|
433
|
+
const logger = new RequestLogger(client, config);
|
|
434
|
+
|
|
435
|
+
// In your request handler:
|
|
436
|
+
const crawlerInfo = detectCrawler(req.headers['user-agent'] ?? '');
|
|
437
|
+
logger.enqueue({
|
|
438
|
+
site_id: config.siteId,
|
|
439
|
+
timestamp: new Date().toISOString(),
|
|
440
|
+
method: req.method,
|
|
441
|
+
path: req.url,
|
|
442
|
+
status_code: res.statusCode,
|
|
443
|
+
response_time_ms: elapsedMs,
|
|
444
|
+
ip_address: req.socket.remoteAddress ?? '',
|
|
445
|
+
user_agent: req.headers['user-agent'] ?? '',
|
|
446
|
+
referer: req.headers.referer ?? null,
|
|
447
|
+
is_ai_crawler: crawlerInfo.isAi,
|
|
448
|
+
crawler_type: crawlerInfo.name,
|
|
449
|
+
crawler_organization: crawlerInfo.organization,
|
|
450
|
+
confidence_score: crawlerInfo.confidence,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Flush on shutdown
|
|
454
|
+
process.on('SIGTERM', () => logger.flush());
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Request logging behavior
|
|
460
|
+
|
|
461
|
+
- Logs are **batched in memory** and flushed every `flushInterval` ms (default 5s) or when `batchSize` entries accumulate (default 50)
|
|
462
|
+
- Logging is **non-blocking** — failures never propagate to the request handler
|
|
463
|
+
- The logger automatically flushes on `SIGTERM`, `SIGINT`, and `beforeExit`
|
|
464
|
+
- The flush timer is **unreferenced** — it will not keep a process alive after all other work is done
|
|
465
|
+
|
|
466
|
+
**Paths skipped by middleware (never logged):**
|
|
467
|
+
- `/_next/` — Next.js build assets
|
|
468
|
+
- `/api/_*` — Internal Next.js API routes
|
|
469
|
+
- `/favicon.ico`
|
|
470
|
+
- Static file extensions: `.svg`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.ico`, `.woff`, `.woff2`, `.ttf`, `.css`, `.js.map`
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Analytics dashboard
|
|
475
|
+
|
|
476
|
+
Visit your `dashboardUrl` after registration to:
|
|
477
|
+
- View AI vs human traffic split over time
|
|
478
|
+
- See which crawlers are visiting and how often
|
|
479
|
+
- Browse agent queries and the answers served
|
|
480
|
+
- Track USDC earnings from sponsored ads
|
|
481
|
+
- Manage API keys and site settings
|
|
482
|
+
|
|
483
|
+
The link is valid for 30 days. Add an email address in the dashboard to establish a permanent login.
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Testing
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
npm test # Unit + integration (hits real dev API)
|
|
491
|
+
npm run test:unit # Unit tests only (mocked API)
|
|
492
|
+
npm run test:integration # Integration tests against deployed dev API
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**Integration tests** use **https://api.apptvty.com** by default: they register a new test site, send real logs, and verify analytics and migrate. No env vars required. Needs network access.
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Publishing the SDK (npm)
|
|
500
|
+
|
|
501
|
+
The SDK is an **npm package**. “Deploy” means publishing to the registry so users can `npm install apptvty`.
|
|
502
|
+
|
|
503
|
+
**Prerequisites**
|
|
504
|
+
|
|
505
|
+
- npm account ([npmjs.com](https://www.npmjs.com/signup))
|
|
506
|
+
- Logged in: `npm login`
|
|
507
|
+
|
|
508
|
+
**Release steps**
|
|
509
|
+
|
|
510
|
+
1. **Bump version** in `package.json` (or use `npm version`):
|
|
511
|
+
```bash
|
|
512
|
+
npm version patch # 0.1.0 → 0.1.1
|
|
513
|
+
# or: npm version minor # 0.1.0 → 0.2.0
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
2. **Run tests** (optional but recommended):
|
|
517
|
+
```bash
|
|
518
|
+
npm run test:unit
|
|
519
|
+
npm run test:integration # uses api.apptvty.com
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
3. **Publish**:
|
|
523
|
+
```bash
|
|
524
|
+
npm publish
|
|
525
|
+
```
|
|
526
|
+
`prepublishOnly` runs `npm run build && npm run typecheck` automatically before the package is uploaded.
|
|
527
|
+
|
|
528
|
+
For a **scoped package** (e.g. `@apptvty/sdk`), make sure `package.json` has `"name": "@apptvty/sdk"` and use:
|
|
529
|
+
```bash
|
|
530
|
+
npm publish --access public
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## License
|
|
536
|
+
|
|
537
|
+
MIT
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApptvtyClient,
|
|
3
|
+
RequestLogger,
|
|
4
|
+
createQueryHandler,
|
|
5
|
+
detectCrawler,
|
|
6
|
+
getClientIp
|
|
7
|
+
} from "./chunk-WATTAPBA.mjs";
|
|
8
|
+
|
|
9
|
+
// src/middleware/express.ts
|
|
10
|
+
var instances = /* @__PURE__ */ new Map();
|
|
11
|
+
function getInstance(config) {
|
|
12
|
+
const key = config.apiKey;
|
|
13
|
+
if (!instances.has(key)) {
|
|
14
|
+
const client = new ApptvtyClient(config);
|
|
15
|
+
const logger = new RequestLogger(client, config);
|
|
16
|
+
instances.set(key, { client, logger });
|
|
17
|
+
}
|
|
18
|
+
return instances.get(key);
|
|
19
|
+
}
|
|
20
|
+
function createExpressMiddleware(config) {
|
|
21
|
+
const { logger } = getInstance(config);
|
|
22
|
+
return function apptvtyLogger(req, res, next) {
|
|
23
|
+
const startMs = Date.now();
|
|
24
|
+
const userAgent = req.headers["user-agent"] ?? "";
|
|
25
|
+
const crawlerInfo = detectCrawler(userAgent);
|
|
26
|
+
const path = req.url ?? "/";
|
|
27
|
+
res.on("finish", () => {
|
|
28
|
+
if (shouldSkip(path)) return;
|
|
29
|
+
const entry = {
|
|
30
|
+
site_id: config.siteId,
|
|
31
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32
|
+
method: req.method ?? "GET",
|
|
33
|
+
path,
|
|
34
|
+
status_code: res.statusCode,
|
|
35
|
+
response_time_ms: Date.now() - startMs,
|
|
36
|
+
ip_address: getClientIp(req.headers),
|
|
37
|
+
user_agent: userAgent,
|
|
38
|
+
referer: req.headers["referer"] ?? null,
|
|
39
|
+
is_ai_crawler: crawlerInfo.isAi,
|
|
40
|
+
crawler_type: crawlerInfo.name,
|
|
41
|
+
crawler_organization: crawlerInfo.organization,
|
|
42
|
+
confidence_score: crawlerInfo.confidence
|
|
43
|
+
};
|
|
44
|
+
logger.enqueue(entry);
|
|
45
|
+
});
|
|
46
|
+
next();
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createExpressQueryHandler(config) {
|
|
50
|
+
const { client } = getInstance(config);
|
|
51
|
+
const handleQuery = createQueryHandler(client, config);
|
|
52
|
+
return async function queryHandler(req, res) {
|
|
53
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
54
|
+
const q = url.searchParams.get("q");
|
|
55
|
+
const lang = url.searchParams.get("lang");
|
|
56
|
+
const surfaceAds = parseBoolParam(url.searchParams.get("surface_ads"), true);
|
|
57
|
+
const aiCrawler = parseBoolParam(url.searchParams.get("ai_crawler"), false);
|
|
58
|
+
const userAgent = req.headers["user-agent"] ?? "";
|
|
59
|
+
const result = await handleQuery({
|
|
60
|
+
query: q,
|
|
61
|
+
lang,
|
|
62
|
+
surface_ads: surfaceAds,
|
|
63
|
+
ai_crawler: aiCrawler,
|
|
64
|
+
userAgent,
|
|
65
|
+
ipAddress: getClientIp(req.headers),
|
|
66
|
+
requestUrl: url.toString()
|
|
67
|
+
});
|
|
68
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
69
|
+
res.setHeader(key, value);
|
|
70
|
+
}
|
|
71
|
+
res.statusCode = result.status;
|
|
72
|
+
res.end(JSON.stringify(result.body));
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function parseBoolParam(value, defaultValue) {
|
|
76
|
+
if (value === null) return defaultValue;
|
|
77
|
+
return value === "1" || value === "true" || value === "yes";
|
|
78
|
+
}
|
|
79
|
+
function shouldSkip(path) {
|
|
80
|
+
return path.startsWith("/_next/") || /\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\.map)$/.test(path);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
createExpressMiddleware,
|
|
85
|
+
createExpressQueryHandler
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=chunk-RGUS6IL6.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware/express.ts"],"sourcesContent":["/**\n * Express / Node.js integration for the Apptvty SDK.\n *\n * Usage:\n *\n * import express from 'express';\n * import { createExpressMiddleware, createExpressQueryHandler } from 'apptvty/express';\n *\n * const app = express();\n * const config = { apiKey: 'ak_...', siteId: 'site_...' };\n *\n * // 1. Log all traffic\n * app.use(createExpressMiddleware(config));\n *\n * // 2. Mount the AEO query page\n * app.get('/query', createExpressQueryHandler(config));\n *\n * Works with any Connect-compatible framework (Express, Fastify via @fastify/express, etc.)\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { ApptvtyClient } from '../client.js';\nimport { detectCrawler } from '../crawler.js';\nimport { RequestLogger, getClientIp } from '../logger.js';\nimport { createQueryHandler } from '../query-handler.js';\nimport type { ApptvtyConfig, RequestLogEntry } from '../types.js';\n\nexport type ConnectMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: unknown) => void,\n) => void;\n\nexport type ConnectHandler = (\n req: IncomingMessage,\n res: ServerResponse,\n) => void | Promise<void>;\n\n// ─── Shared singleton instances per config ────────────────────────────────────\n\nconst instances = new Map<string, { client: ApptvtyClient; logger: RequestLogger }>();\n\nfunction getInstance(config: ApptvtyConfig) {\n const key = config.apiKey;\n if (!instances.has(key)) {\n const client = new ApptvtyClient(config);\n const logger = new RequestLogger(client, config);\n instances.set(key, { client, logger });\n }\n return instances.get(key)!;\n}\n\n// ─── Traffic logging middleware ───────────────────────────────────────────────\n\n/**\n * Express middleware that logs every request to Apptvty.\n * Captures: method, path, status, response time, user-agent, IP, crawler classification.\n *\n * Mount this before your routes so all traffic is captured.\n *\n * @example\n * app.use(createExpressMiddleware({ apiKey: 'ak_...', siteId: 'site_...' }));\n */\nexport function createExpressMiddleware(config: ApptvtyConfig): ConnectMiddleware {\n const { logger } = getInstance(config);\n\n return function apptvtyLogger(req, res, next) {\n const startMs = Date.now();\n const userAgent = req.headers['user-agent'] ?? '';\n const crawlerInfo = detectCrawler(userAgent);\n const path = req.url ?? '/';\n\n // Intercept response finish to capture status code and timing\n res.on('finish', () => {\n if (shouldSkip(path)) return;\n\n const entry: RequestLogEntry = {\n site_id: config.siteId,\n timestamp: new Date().toISOString(),\n method: req.method ?? 'GET',\n path,\n status_code: res.statusCode,\n response_time_ms: Date.now() - startMs,\n ip_address: getClientIp(req.headers as Record<string, string | string[] | undefined>),\n user_agent: userAgent,\n referer: (req.headers['referer'] as string) ?? null,\n is_ai_crawler: crawlerInfo.isAi,\n crawler_type: crawlerInfo.name,\n crawler_organization: crawlerInfo.organization,\n confidence_score: crawlerInfo.confidence,\n };\n\n logger.enqueue(entry);\n });\n\n next();\n };\n}\n\n// ─── Query endpoint handler ───────────────────────────────────────────────────\n\n/**\n * Express route handler for the AEO query endpoint.\n *\n * Mount this at the path configured in your dashboard (default: /query).\n *\n * @example\n * app.get('/query', createExpressQueryHandler({ apiKey: 'ak_...', siteId: 'site_...' }));\n */\nexport function createExpressQueryHandler(config: ApptvtyConfig): ConnectHandler {\n const { client } = getInstance(config);\n const handleQuery = createQueryHandler(client, config);\n\n return async function queryHandler(req, res) {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);\n const q = url.searchParams.get('q');\n const lang = url.searchParams.get('lang');\n const surfaceAds = parseBoolParam(url.searchParams.get('surface_ads'), true);\n const aiCrawler = parseBoolParam(url.searchParams.get('ai_crawler'), false);\n const userAgent = req.headers['user-agent'] ?? '';\n\n const result = await handleQuery({\n query: q,\n lang,\n surface_ads: surfaceAds,\n ai_crawler: aiCrawler,\n userAgent,\n ipAddress: getClientIp(req.headers as Record<string, string | string[] | undefined>),\n requestUrl: url.toString(),\n });\n\n for (const [key, value] of Object.entries(result.headers)) {\n res.setHeader(key, value);\n }\n res.statusCode = result.status;\n res.end(JSON.stringify(result.body));\n };\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction parseBoolParam(value: string | null, defaultValue: boolean): boolean {\n if (value === null) return defaultValue;\n return value === '1' || value === 'true' || value === 'yes';\n}\n\nfunction shouldSkip(path: string): boolean {\n return (\n path.startsWith('/_next/') ||\n /\\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\\.map)$/.test(path)\n );\n}\n"],"mappings":";;;;;;;;;AAwCA,IAAM,YAAY,oBAAI,IAA8D;AAEpF,SAAS,YAAY,QAAuB;AAC1C,QAAM,MAAM,OAAO;AACnB,MAAI,CAAC,UAAU,IAAI,GAAG,GAAG;AACvB,UAAM,SAAS,IAAI,cAAc,MAAM;AACvC,UAAM,SAAS,IAAI,cAAc,QAAQ,MAAM;AAC/C,cAAU,IAAI,KAAK,EAAE,QAAQ,OAAO,CAAC;AAAA,EACvC;AACA,SAAO,UAAU,IAAI,GAAG;AAC1B;AAaO,SAAS,wBAAwB,QAA0C;AAChF,QAAM,EAAE,OAAO,IAAI,YAAY,MAAM;AAErC,SAAO,SAAS,cAAc,KAAK,KAAK,MAAM;AAC5C,UAAM,UAAU,KAAK,IAAI;AACzB,UAAM,YAAY,IAAI,QAAQ,YAAY,KAAK;AAC/C,UAAM,cAAc,cAAc,SAAS;AAC3C,UAAM,OAAO,IAAI,OAAO;AAGxB,QAAI,GAAG,UAAU,MAAM;AACrB,UAAI,WAAW,IAAI,EAAG;AAEtB,YAAM,QAAyB;AAAA,QAC7B,SAAS,OAAO;AAAA,QAChB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClC,QAAQ,IAAI,UAAU;AAAA,QACtB;AAAA,QACA,aAAa,IAAI;AAAA,QACjB,kBAAkB,KAAK,IAAI,IAAI;AAAA,QAC/B,YAAY,YAAY,IAAI,OAAwD;AAAA,QACpF,YAAY;AAAA,QACZ,SAAU,IAAI,QAAQ,SAAS,KAAgB;AAAA,QAC/C,eAAe,YAAY;AAAA,QAC3B,cAAc,YAAY;AAAA,QAC1B,sBAAsB,YAAY;AAAA,QAClC,kBAAkB,YAAY;AAAA,MAChC;AAEA,aAAO,QAAQ,KAAK;AAAA,IACtB,CAAC;AAED,SAAK;AAAA,EACP;AACF;AAYO,SAAS,0BAA0B,QAAuC;AAC/E,QAAM,EAAE,OAAO,IAAI,YAAY,MAAM;AACrC,QAAM,cAAc,mBAAmB,QAAQ,MAAM;AAErD,SAAO,eAAe,aAAa,KAAK,KAAK;AAC3C,UAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,IAAI,QAAQ,QAAQ,WAAW,EAAE;AAC/E,UAAM,IAAI,IAAI,aAAa,IAAI,GAAG;AAClC,UAAM,OAAO,IAAI,aAAa,IAAI,MAAM;AACxC,UAAM,aAAa,eAAe,IAAI,aAAa,IAAI,aAAa,GAAG,IAAI;AAC3E,UAAM,YAAY,eAAe,IAAI,aAAa,IAAI,YAAY,GAAG,KAAK;AAC1E,UAAM,YAAY,IAAI,QAAQ,YAAY,KAAK;AAE/C,UAAM,SAAS,MAAM,YAAY;AAAA,MAC/B,OAAO;AAAA,MACP;AAAA,MACA,aAAa;AAAA,MACb,YAAY;AAAA,MACZ;AAAA,MACA,WAAW,YAAY,IAAI,OAAwD;AAAA,MACnF,YAAY,IAAI,SAAS;AAAA,IAC3B,CAAC;AAED,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,OAAO,GAAG;AACzD,UAAI,UAAU,KAAK,KAAK;AAAA,IAC1B;AACA,QAAI,aAAa,OAAO;AACxB,QAAI,IAAI,KAAK,UAAU,OAAO,IAAI,CAAC;AAAA,EACrC;AACF;AAIA,SAAS,eAAe,OAAsB,cAAgC;AAC5E,MAAI,UAAU,KAAM,QAAO;AAC3B,SAAO,UAAU,OAAO,UAAU,UAAU,UAAU;AACxD;AAEA,SAAS,WAAW,MAAuB;AACzC,SACE,KAAK,WAAW,SAAS,KACzB,4DAA4D,KAAK,IAAI;AAEzE;","names":[]}
|