Skip to content

Commit 4e63ae6

Browse files
committed
Align OSS wrapper with reduced read API
1 parent 15f410f commit 4e63ae6

4 files changed

Lines changed: 19 additions & 141 deletions

File tree

README.md

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Web analytics your AI agent can read. Same idea as Google Analytics — add a JS snippet to your site — but instead of dashboards, your agent queries the data via CLI or API.
44

5-
Pair it with [OpenClaw](https://openclaw.com) or any coding agent and it becomes a growth machine — your agent checks traffic, runs A/B tests, finds funnel drop-offs, and tells you what to fix. While you sleep.
5+
Pair it with [OpenClaw](https://openclaw.ai) or any coding agent and it becomes a growth machine — your agent checks traffic, runs A/B tests, finds funnel drop-offs, and tells you what to fix. While you sleep.
66

77
Self-host on Cloudflare Workers (free tier) or Node.js. Or use the [managed service](https://app.agentanalytics.sh) if you don't want to run infrastructure.
88

@@ -112,15 +112,15 @@ window.aa.page('Dashboard');
112112

113113
---
114114

115-
## Query Your Data
115+
## Read Your Data
116116

117117
Your agent reads the data instead of you opening a dashboard:
118118

119119
```bash
120120
# Point CLI at your instance
121121
npx @agent-analytics/cli login --token your-secret-read-key --url https://your-server.com
122122

123-
# Query
123+
# Read
124124
npx @agent-analytics/cli stats my-site # Last 7 days
125125
npx @agent-analytics/cli stats my-site --days 30 # Last 30 days
126126
npx @agent-analytics/cli events my-site # Recent events
@@ -174,7 +174,7 @@ Two types of keys — same model as Mixpanel:
174174
| Key | Purpose | Visibility | Used by |
175175
|-----|---------|------------|---------|
176176
| **Project Token** (`pt_...`) | Identifies which project events belong to | Public (embedded in JS snippet) | `tracker.js`, `/track` |
177-
| **API Key** | Read access to query stats | **Private** (keep secret) | CLI, `/stats`, `/events`, `/query` |
177+
| **API Key** | Read access to project lists, stats, and recent events | **Private** (keep secret) | CLI, `/projects`, `/stats`, `/events` |
178178

179179
### Tracking Events
180180

@@ -229,7 +229,7 @@ curl -X POST "https://your-server.com/track/batch" \
229229
| `events[].user_id` | | User identifier |
230230
| `events[].timestamp` | | Unix ms (defaults to now) |
231231

232-
### Querying Data
232+
### Reading Data
233233

234234
#### `GET /stats` — Aggregated overview
235235

@@ -276,47 +276,14 @@ curl "https://your-server.com/events?project=my-site&event=page_view&days=7&limi
276276
```
277277
</details>
278278

279-
#### `POST /query` — Flexible analytics query
280-
281-
The power endpoint. Supports metrics, grouping, filtering, and sorting.
282-
283-
```bash
284-
curl -X POST "https://your-server.com/query" \
285-
-H "X-API-Key: YOUR_API_KEY" \
286-
-H "Content-Type: application/json" \
287-
-d '{
288-
"project": "my-site",
289-
"metrics": ["event_count", "unique_users"],
290-
"group_by": ["event", "date"],
291-
"filters": [
292-
{ "field": "event", "op": "eq", "value": "page_view" },
293-
{ "field": "properties.browser", "op": "eq", "value": "chrome" }
294-
],
295-
"date_from": "2026-01-01",
296-
"date_to": "2026-01-31",
297-
"order_by": "event_count",
298-
"order": "desc",
299-
"limit": 50
300-
}'
301-
```
302-
303-
| Parameter | Description |
304-
|-----------|-------------|
305-
| `metrics` | `event_count`, `unique_users` |
306-
| `group_by` | `event`, `date`, `user_id` |
307-
| `filters[].op` | `eq`, `neq`, `gt`, `lt`, `gte`, `lte` |
308-
| `filters[].field` | `event`, `user_id`, `date`, or `properties.*` for JSON property filters |
309-
| `order_by` | Any metric or group_by field |
310-
| `limit` | Max 1000 rows (default: 100) |
311-
312-
#### `GET /properties` — Discover events & property keys
279+
#### `GET /projects` — List projects
313280

314281
```bash
315-
curl "https://your-server.com/properties?project=my-site&days=30" \
282+
curl "https://your-server.com/projects" \
316283
-H "X-API-Key: YOUR_API_KEY"
317284
```
318285

319-
Returns event names with counts, first/last seen dates, and all known property keys. Useful for building dynamic queries.
286+
Returns the projects already present in your self-hosted instance so your agent can pick the right site before reading stats or recent events.
320287

321288
### Utility
322289

@@ -350,7 +317,7 @@ flowchart TB
350317
end
351318
352319
website -- "POST /track" --> auth
353-
agent -- "GET /stats · POST /query" --> auth
320+
agent -- "GET /projects · GET /stats · GET /events" --> auth
354321
auth --> cf & node
355322
cf --> handler
356323
node --> handler
@@ -374,7 +341,7 @@ src/ (this repo — platform glue + auth)
374341
src/
375342
handler.js — Platform-agnostic request routing + response building
376343
db/
377-
adapter.js — Date helpers, shared query logic
344+
adapter.js — Date helpers and adapter contracts
378345
d1.js — Cloudflare D1 adapter (D1Adapter, validatePropertyKey)
379346
tracker.js — Client-side tracking script (served at GET /tracker.js)
380347
ulid.js — ULID generation for event IDs

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/__tests__/server.test.mjs

Lines changed: 7 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -243,114 +243,25 @@ describe('GET /events', () => {
243243
});
244244
});
245245

246-
describe('GET /sessions', () => {
247-
it('returns sessions for project', async () => {
246+
describe('removed OSS analytics endpoints', () => {
247+
it('returns 404 for /sessions', async () => {
248248
const { response } = await handler(get(`/sessions?project=${PROJECT}`, authHeaders));
249-
expect(response.status).toBe(200);
250-
const data = await response.json();
251-
expect(data.project).toBe(PROJECT);
252-
expect(Array.isArray(data.sessions)).toBe(true);
253-
expect(data.sessions.length).toBeGreaterThan(0);
254-
});
255-
256-
it('rejects without API key', async () => {
257-
const { response } = await handler(get(`/sessions?project=${PROJECT}`));
258-
expect(response.status).toBe(401);
259-
});
260-
261-
it('rejects without project param', async () => {
262-
const { response } = await handler(get('/sessions', authHeaders));
263-
expect(response.status).toBe(400);
264-
});
265-
});
266-
267-
describe('POST /query', () => {
268-
it('returns query results', async () => {
269-
const req = postJSON('/query', {
270-
project: PROJECT,
271-
metrics: ['event_count', 'unique_users'],
272-
group_by: ['event'],
273-
});
274-
req.headers.set('X-API-Key', API_KEY);
275-
276-
const { response } = await handler(req);
277-
expect(response.status).toBe(200);
278-
const data = await response.json();
279-
expect(data.project).toBe(PROJECT);
280-
expect(data.rows).toBeDefined();
281-
expect(data.rows.length).toBeGreaterThan(0);
282-
expect(data.count).toBeGreaterThan(0);
283-
});
284-
285-
it('supports filters', async () => {
286-
const req = postJSON('/query', {
287-
project: PROJECT,
288-
metrics: ['event_count'],
289-
filters: [{ field: 'event', op: 'eq', value: 'page_view' }],
290-
});
291-
req.headers.set('X-API-Key', API_KEY);
292-
293-
const { response } = await handler(req);
294-
expect(response.status).toBe(200);
295-
const data = await response.json();
296-
expect(data.rows).toBeDefined();
249+
expect(response.status).toBe(404);
297250
});
298251

299-
it('supports property filters', async () => {
252+
it('returns 404 for /query', async () => {
300253
const req = postJSON('/query', {
301254
project: PROJECT,
302255
metrics: ['event_count'],
303-
filters: [{ field: 'properties.path', op: 'eq', value: '/home' }],
304-
});
305-
req.headers.set('X-API-Key', API_KEY);
306-
307-
const { response } = await handler(req);
308-
expect(response.status).toBe(200);
309-
});
310-
311-
it('rejects without API key', async () => {
312-
const { response } = await handler(postJSON('/query', { project: PROJECT }));
313-
expect(response.status).toBe(401);
314-
});
315-
316-
it('rejects without project', async () => {
317-
const req = postJSON('/query', { metrics: ['event_count'] });
318-
req.headers.set('X-API-Key', API_KEY);
319-
const { response } = await handler(req);
320-
expect(response.status).toBe(400);
321-
});
322-
323-
it('rejects invalid metric', async () => {
324-
const req = postJSON('/query', {
325-
project: PROJECT,
326-
metrics: ['bogus'],
327256
});
328257
req.headers.set('X-API-Key', API_KEY);
329258
const { response } = await handler(req);
330-
expect(response.status).toBe(400);
259+
expect(response.status).toBe(404);
331260
});
332-
});
333261

334-
describe('GET /properties', () => {
335-
it('returns event names and property keys', async () => {
262+
it('returns 404 for /properties', async () => {
336263
const { response } = await handler(get(`/properties?project=${PROJECT}`, authHeaders));
337-
expect(response.status).toBe(200);
338-
const data = await response.json();
339-
expect(data.project).toBe(PROJECT);
340-
expect(Array.isArray(data.events)).toBe(true);
341-
expect(data.events.length).toBeGreaterThan(0);
342-
expect(Array.isArray(data.property_keys)).toBe(true);
343-
expect(data.property_keys).toContain('path');
344-
});
345-
346-
it('rejects without API key', async () => {
347-
const { response } = await handler(get(`/properties?project=${PROJECT}`));
348-
expect(response.status).toBe(401);
349-
});
350-
351-
it('rejects without project param', async () => {
352-
const { response } = await handler(get('/properties', authHeaders));
353-
expect(response.status).toBe(400);
264+
expect(response.status).toBe(404);
354265
});
355266
});
356267

src/auth.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function makeValidateWrite(projectTokensStr) {
2424
}
2525

2626
/**
27-
* Create a read validator for query endpoints (/stats, /events, /query, /properties).
27+
* Create a read validator for OSS read endpoints (/projects, /stats, /events).
2828
* API key via X-API-Key header or ?key= query param.
2929
*/
3030
export function makeValidateRead(apiKeysStr) {

0 commit comments

Comments
 (0)