@seneris/nosework 0.1.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 +535 -0
- package/dist/client/errors.d.ts +49 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +173 -0
- package/dist/client/errors.js.map +1 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +29 -0
- package/dist/client.js.map +1 -0
- package/dist/error.d.ts +40 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +248 -0
- package/dist/error.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/query.d.ts +48 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +471 -0
- package/dist/query.js.map +1 -0
- package/dist/track.d.ts +4 -0
- package/dist/track.d.ts.map +1 -0
- package/dist/track.js +74 -0
- package/dist/track.js.map +1 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ua.d.ts +3 -0
- package/dist/ua.d.ts.map +1 -0
- package/dist/ua.js +40 -0
- package/dist/ua.js.map +1 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +112 -0
- package/dist/utils.js.map +1 -0
- package/package.json +54 -0
- package/prisma/schema.prisma +145 -0
package/README.md
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
# nosework
|
|
2
|
+
|
|
3
|
+
Privacy-focused, self-hosted analytics for your app suite.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Cookieless tracking** - No consent banners needed
|
|
8
|
+
- **City-level geolocation** - Via Vercel's geo headers
|
|
9
|
+
- **GDPR compliant** - No PII stored, IPs are hashed
|
|
10
|
+
- **Multi-site support** - Track all your apps with one shared database
|
|
11
|
+
- **Full API** - Query your analytics data programmatically
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
17
|
+
│ App 1 │ │ App 2 │ │ App N │
|
|
18
|
+
│ (Next.js) │ │ (Next.js) │ │ (Next.js) │
|
|
19
|
+
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
|
20
|
+
│ │ │
|
|
21
|
+
│ import { trackPageView } │
|
|
22
|
+
│ from 'nosework' │
|
|
23
|
+
│ │ │
|
|
24
|
+
└────────────────┼────────────────┘
|
|
25
|
+
│
|
|
26
|
+
┌─────────▼─────────┐
|
|
27
|
+
│ Shared Neon DB │
|
|
28
|
+
│ (PostgreSQL) │
|
|
29
|
+
└───────────────────┘
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Each app imports nosework and writes directly to a shared PostgreSQL database. No intermediate service needed.
|
|
33
|
+
|
|
34
|
+
### Why Each App Needs a Tracking Endpoint
|
|
35
|
+
|
|
36
|
+
The browser (client-side) cannot:
|
|
37
|
+
- Connect directly to PostgreSQL
|
|
38
|
+
- Access HTTP headers (IP address, User-Agent, geo data)
|
|
39
|
+
|
|
40
|
+
So each app needs a small API endpoint that:
|
|
41
|
+
1. Receives tracking requests from the browser
|
|
42
|
+
2. Reads IP/User-Agent/geo from HTTP headers
|
|
43
|
+
3. Calls `trackPageView()` to write to the shared DB
|
|
44
|
+
|
|
45
|
+
This is typically ~20 lines of code per app.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Geolocation: Vercel Headers
|
|
50
|
+
|
|
51
|
+
**Important:** This package is designed for apps hosted on **Vercel**.
|
|
52
|
+
|
|
53
|
+
Vercel automatically adds geolocation headers to every request:
|
|
54
|
+
|
|
55
|
+
| Header | Example | Description |
|
|
56
|
+
|--------|---------|-------------|
|
|
57
|
+
| `x-vercel-ip-country` | `US` | Country code |
|
|
58
|
+
| `x-vercel-ip-country-region` | `CA` | Region/state |
|
|
59
|
+
| `x-vercel-ip-city` | `San Francisco` | City name |
|
|
60
|
+
|
|
61
|
+
These headers are:
|
|
62
|
+
- **Free** - No third-party accounts or API keys needed
|
|
63
|
+
- **Automatic** - Available on every request when deployed to Vercel
|
|
64
|
+
- **Accurate** - Powered by Vercel's edge network
|
|
65
|
+
|
|
66
|
+
### Local Development
|
|
67
|
+
|
|
68
|
+
Geo headers are **not available** in local development (`localhost`). Location data will be `null` when developing locally - this is expected and won't affect functionality.
|
|
69
|
+
|
|
70
|
+
### Non-Vercel Hosting
|
|
71
|
+
|
|
72
|
+
If you're not using Vercel, you have options:
|
|
73
|
+
1. Skip location data (everything else works fine)
|
|
74
|
+
2. Use a GeoIP service/database and pass the data to `trackPageView()`
|
|
75
|
+
3. Check if your hosting provider offers similar geo headers
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Quick Start
|
|
80
|
+
|
|
81
|
+
### 1. Install
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
bun add nosework
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 2. Set Environment Variables
|
|
88
|
+
|
|
89
|
+
Add to your `.env`:
|
|
90
|
+
|
|
91
|
+
```env
|
|
92
|
+
DATABASE_URL="postgresql://user:pass@host/dbname"
|
|
93
|
+
ANALYTICS_SITE_ID="my-app" # Unique identifier for this app
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**MoopySuite Users:** If you're using MoopySuite OAuth, use your existing `MOOPY_CLIENT_ID` as the site identifier - no new env var needed! See [MoopySuite Integration](./docs/MOOPYSUITE_INTEGRATION.md).
|
|
97
|
+
|
|
98
|
+
### 3. Run Database Migration
|
|
99
|
+
|
|
100
|
+
First time only (or when updating nosework):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
bunx prisma migrate deploy --schema=./node_modules/nosework/prisma/schema.prisma
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 4. Add Tracking Endpoint
|
|
107
|
+
|
|
108
|
+
Create `app/api/analytics/track/route.ts`:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { trackPageView } from 'nosework';
|
|
112
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
113
|
+
|
|
114
|
+
export async function POST(request: NextRequest) {
|
|
115
|
+
try {
|
|
116
|
+
const { url, referrer } = await request.json();
|
|
117
|
+
|
|
118
|
+
await trackPageView({
|
|
119
|
+
siteId: process.env.ANALYTICS_SITE_ID!,
|
|
120
|
+
url,
|
|
121
|
+
referrer,
|
|
122
|
+
// For visitor hashing
|
|
123
|
+
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
|
|
124
|
+
userAgent: request.headers.get('user-agent'),
|
|
125
|
+
// Vercel geo headers (automatically provided when deployed)
|
|
126
|
+
country: request.headers.get('x-vercel-ip-country'),
|
|
127
|
+
countryCode: request.headers.get('x-vercel-ip-country'),
|
|
128
|
+
region: request.headers.get('x-vercel-ip-country-region'),
|
|
129
|
+
city: request.headers.get('x-vercel-ip-city'),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return NextResponse.json({ ok: true });
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('Analytics error:', error);
|
|
135
|
+
return NextResponse.json({ ok: false }, { status: 500 });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 5. Add Client Component
|
|
141
|
+
|
|
142
|
+
Create `components/Analytics.tsx`:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
'use client';
|
|
146
|
+
|
|
147
|
+
import { usePathname, useSearchParams } from 'next/navigation';
|
|
148
|
+
import { useEffect, useRef } from 'react';
|
|
149
|
+
|
|
150
|
+
export function Analytics() {
|
|
151
|
+
const pathname = usePathname();
|
|
152
|
+
const searchParams = useSearchParams();
|
|
153
|
+
const initialLoad = useRef(true);
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
// Track page view
|
|
157
|
+
const trackPageView = () => {
|
|
158
|
+
fetch('/api/analytics/track', {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
url: window.location.href,
|
|
163
|
+
referrer: initialLoad.current ? document.referrer : null,
|
|
164
|
+
}),
|
|
165
|
+
keepalive: true, // Ensures request completes even on navigation
|
|
166
|
+
}).catch(() => {
|
|
167
|
+
// Silently fail - analytics should never break the app
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
initialLoad.current = false;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
trackPageView();
|
|
174
|
+
}, [pathname, searchParams]);
|
|
175
|
+
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 6. Add to Layout
|
|
181
|
+
|
|
182
|
+
In `app/layout.tsx`:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { Analytics } from '@/components/Analytics';
|
|
186
|
+
import { Suspense } from 'react';
|
|
187
|
+
|
|
188
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
189
|
+
return (
|
|
190
|
+
<html>
|
|
191
|
+
<body>
|
|
192
|
+
{children}
|
|
193
|
+
<Suspense fallback={null}>
|
|
194
|
+
<Analytics />
|
|
195
|
+
</Suspense>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
That's it! Your app is now tracking page views with location data.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Alternative: Middleware-Only Tracking
|
|
207
|
+
|
|
208
|
+
If you prefer not to use a client component, you can track in Next.js middleware. This is simpler but won't capture client-side navigations in SPAs.
|
|
209
|
+
|
|
210
|
+
Create `middleware.ts`:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { trackPageView } from 'nosework';
|
|
214
|
+
import { NextResponse } from 'next/server';
|
|
215
|
+
import type { NextRequest } from 'next/server';
|
|
216
|
+
|
|
217
|
+
export async function middleware(request: NextRequest) {
|
|
218
|
+
// Only track page requests, not API/static files
|
|
219
|
+
const { pathname } = request.nextUrl;
|
|
220
|
+
|
|
221
|
+
if (
|
|
222
|
+
pathname.startsWith('/api') ||
|
|
223
|
+
pathname.startsWith('/_next') ||
|
|
224
|
+
pathname.includes('.')
|
|
225
|
+
) {
|
|
226
|
+
return NextResponse.next();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Fire and forget - don't block the response
|
|
230
|
+
trackPageView({
|
|
231
|
+
siteId: process.env.ANALYTICS_SITE_ID!,
|
|
232
|
+
url: request.url,
|
|
233
|
+
referrer: request.headers.get('referer'),
|
|
234
|
+
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
|
|
235
|
+
userAgent: request.headers.get('user-agent'),
|
|
236
|
+
country: request.headers.get('x-vercel-ip-country'),
|
|
237
|
+
countryCode: request.headers.get('x-vercel-ip-country'),
|
|
238
|
+
region: request.headers.get('x-vercel-ip-country-region'),
|
|
239
|
+
city: request.headers.get('x-vercel-ip-city'),
|
|
240
|
+
}).catch(() => {});
|
|
241
|
+
|
|
242
|
+
return NextResponse.next();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const config = {
|
|
246
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
247
|
+
};
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Tradeoffs:**
|
|
251
|
+
|
|
252
|
+
| Approach | Pros | Cons |
|
|
253
|
+
|----------|------|------|
|
|
254
|
+
| Client + API | Catches all navigations, including SPA | More setup (~2 files) |
|
|
255
|
+
| Middleware only | Single file, simpler | Misses client-side navigations |
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Tracking Custom Events
|
|
260
|
+
|
|
261
|
+
Beyond page views, you can track custom events:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { trackEvent } from 'nosework';
|
|
265
|
+
|
|
266
|
+
// In an API route or server action
|
|
267
|
+
await trackEvent({
|
|
268
|
+
siteId: process.env.ANALYTICS_SITE_ID!,
|
|
269
|
+
name: 'signup',
|
|
270
|
+
properties: { plan: 'pro', source: 'landing-page' },
|
|
271
|
+
ip: request.headers.get('x-forwarded-for'),
|
|
272
|
+
userAgent: request.headers.get('user-agent'),
|
|
273
|
+
userId: user?.id, // Optional - link to your user
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Common events to track:
|
|
278
|
+
- `signup` - User registration
|
|
279
|
+
- `login` - User authentication
|
|
280
|
+
- `purchase` - Completed transaction
|
|
281
|
+
- `button_click` - Important UI interactions
|
|
282
|
+
- `form_submit` - Form completions
|
|
283
|
+
- `error` - Client-side errors
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Querying Analytics
|
|
288
|
+
|
|
289
|
+
### Basic Stats
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
import { getStats } from 'nosework';
|
|
293
|
+
|
|
294
|
+
const stats = await getStats({
|
|
295
|
+
siteId: 'my-app',
|
|
296
|
+
startDate: new Date('2024-01-01'),
|
|
297
|
+
endDate: new Date(),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Returns:
|
|
301
|
+
// {
|
|
302
|
+
// pageViews: 12345,
|
|
303
|
+
// visitors: 5678,
|
|
304
|
+
// sessions: 7890,
|
|
305
|
+
// bounceRate: 42.5
|
|
306
|
+
// }
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Top Pages
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import { getTopPages } from 'nosework';
|
|
313
|
+
|
|
314
|
+
const pages = await getTopPages({
|
|
315
|
+
siteId: 'my-app',
|
|
316
|
+
startDate,
|
|
317
|
+
endDate,
|
|
318
|
+
limit: 10,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Returns:
|
|
322
|
+
// [
|
|
323
|
+
// { pathname: '/', pageViews: 5000, visitors: 3000 },
|
|
324
|
+
// { pathname: '/pricing', pageViews: 2000, visitors: 1500 },
|
|
325
|
+
// ...
|
|
326
|
+
// ]
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Location Breakdown
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
import { getLocations } from 'nosework';
|
|
333
|
+
|
|
334
|
+
const locations = await getLocations({
|
|
335
|
+
siteId: 'my-app',
|
|
336
|
+
startDate,
|
|
337
|
+
endDate,
|
|
338
|
+
limit: 20,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Returns:
|
|
342
|
+
// [
|
|
343
|
+
// { country: 'United States', countryCode: 'US', city: 'New York', pageViews: 1000, visitors: 500 },
|
|
344
|
+
// { country: 'Netherlands', countryCode: 'NL', city: 'Amsterdam', pageViews: 800, visitors: 400 },
|
|
345
|
+
// ...
|
|
346
|
+
// ]
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Traffic Sources
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import { getReferrers } from 'nosework';
|
|
353
|
+
|
|
354
|
+
const referrers = await getReferrers({
|
|
355
|
+
siteId: 'my-app',
|
|
356
|
+
startDate,
|
|
357
|
+
endDate,
|
|
358
|
+
limit: 10,
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Device/Browser Breakdown
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { getDevices } from 'nosework';
|
|
366
|
+
|
|
367
|
+
const devices = await getDevices({
|
|
368
|
+
siteId: 'my-app',
|
|
369
|
+
startDate,
|
|
370
|
+
endDate,
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Time Series (for charts)
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
import { getTimeSeries } from 'nosework';
|
|
378
|
+
|
|
379
|
+
const data = await getTimeSeries({
|
|
380
|
+
siteId: 'my-app',
|
|
381
|
+
startDate,
|
|
382
|
+
endDate,
|
|
383
|
+
interval: 'day', // 'hour' | 'day' | 'week' | 'month'
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Returns:
|
|
387
|
+
// [
|
|
388
|
+
// { date: '2024-01-01', pageViews: 100, visitors: 50 },
|
|
389
|
+
// { date: '2024-01-02', pageViews: 120, visitors: 60 },
|
|
390
|
+
// ...
|
|
391
|
+
// ]
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Site Management
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
import { listSites, getOrCreateSite } from 'nosework';
|
|
398
|
+
|
|
399
|
+
// List all tracked sites
|
|
400
|
+
const sites = await listSites();
|
|
401
|
+
|
|
402
|
+
// Get or create a site (useful for initial setup)
|
|
403
|
+
const site = await getOrCreateSite('example.com', 'My Example Site');
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Privacy Design
|
|
409
|
+
|
|
410
|
+
nosework is designed to be privacy-friendly by default:
|
|
411
|
+
|
|
412
|
+
### No Cookies
|
|
413
|
+
Visitors are identified by a hash of IP + User-Agent + daily salt. This hash rotates daily, so you can't track users across days (by design).
|
|
414
|
+
|
|
415
|
+
### No PII Storage
|
|
416
|
+
- IP addresses are **never stored** - only used for hashing
|
|
417
|
+
- User-Agent strings are parsed into categories (e.g., "Chrome", "Windows") - raw strings are not stored
|
|
418
|
+
- The visitor hash cannot be reversed to identify the original IP
|
|
419
|
+
|
|
420
|
+
### Session Inference
|
|
421
|
+
Sessions are inferred using a 30-minute window hash. No session cookies needed.
|
|
422
|
+
|
|
423
|
+
### Bot Filtering
|
|
424
|
+
Known bots (Googlebot, crawlers, etc.) are automatically flagged and excluded from statistics.
|
|
425
|
+
|
|
426
|
+
### GDPR Compliance
|
|
427
|
+
Because no cookies are used and no PII is stored, you typically don't need:
|
|
428
|
+
- Cookie consent banners
|
|
429
|
+
- Privacy policy updates for analytics
|
|
430
|
+
- Data processing agreements
|
|
431
|
+
|
|
432
|
+
*Note: Consult with a legal professional for your specific situation.*
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## API Reference
|
|
437
|
+
|
|
438
|
+
### Tracking Functions
|
|
439
|
+
|
|
440
|
+
#### `trackPageView(options)`
|
|
441
|
+
Track a page view.
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
interface TrackPageViewOptions {
|
|
445
|
+
siteId: string; // Required: Your site identifier
|
|
446
|
+
url: string; // Required: Full URL of the page
|
|
447
|
+
referrer?: string; // Optional: Referring URL
|
|
448
|
+
ip?: string; // Optional: Visitor IP (for hashing)
|
|
449
|
+
userAgent?: string; // Optional: Browser user agent
|
|
450
|
+
userId?: string; // Optional: Your app's user ID
|
|
451
|
+
// Geo data (from Vercel headers)
|
|
452
|
+
country?: string; // Optional: Country name
|
|
453
|
+
countryCode?: string; // Optional: Country code (e.g., "US")
|
|
454
|
+
region?: string; // Optional: Region/state
|
|
455
|
+
city?: string; // Optional: City name
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
#### `trackEvent(options)`
|
|
460
|
+
Track a custom event.
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
interface TrackEventOptions {
|
|
464
|
+
siteId: string; // Required: Your site identifier
|
|
465
|
+
name: string; // Required: Event name
|
|
466
|
+
properties?: object; // Optional: Event metadata
|
|
467
|
+
url?: string; // Optional: Page URL
|
|
468
|
+
ip?: string; // Optional: Visitor IP
|
|
469
|
+
userAgent?: string; // Optional: Browser user agent
|
|
470
|
+
userId?: string; // Optional: Your app's user ID
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Query Functions
|
|
475
|
+
|
|
476
|
+
All query functions accept:
|
|
477
|
+
```typescript
|
|
478
|
+
interface QueryOptions {
|
|
479
|
+
siteId: string;
|
|
480
|
+
startDate: Date;
|
|
481
|
+
endDate: Date;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
interface PaginatedQueryOptions extends QueryOptions {
|
|
485
|
+
limit?: number; // Default: 10-20 depending on function
|
|
486
|
+
offset?: number;
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
| Function | Returns |
|
|
491
|
+
|----------|---------|
|
|
492
|
+
| `getStats(options)` | `{ pageViews, visitors, sessions, bounceRate }` |
|
|
493
|
+
| `getTopPages(options)` | `[{ pathname, pageViews, visitors }]` |
|
|
494
|
+
| `getLocations(options)` | `[{ country, countryCode, city, pageViews, visitors }]` |
|
|
495
|
+
| `getReferrers(options)` | `[{ referrer, pageViews, visitors }]` |
|
|
496
|
+
| `getDevices(options)` | `[{ device, browser, os, pageViews, visitors }]` |
|
|
497
|
+
| `getTimeSeries(options)` | `[{ date, pageViews, visitors }]` |
|
|
498
|
+
| `listSites()` | `[{ id, name, domain, createdAt }]` |
|
|
499
|
+
| `getOrCreateSite(domain, name?)` | `{ id, name, domain }` |
|
|
500
|
+
|
|
501
|
+
### Utility Functions
|
|
502
|
+
|
|
503
|
+
| Function | Description |
|
|
504
|
+
|----------|-------------|
|
|
505
|
+
| `getClient()` | Get the Prisma client for custom queries |
|
|
506
|
+
| `disconnect()` | Disconnect from the database |
|
|
507
|
+
| `isBot(userAgent)` | Check if a user-agent is a bot |
|
|
508
|
+
| `parseUserAgent(ua)` | Parse a user-agent string |
|
|
509
|
+
| `cleanupOldSalts()` | Remove daily salts older than 7 days |
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## Database Schema
|
|
514
|
+
|
|
515
|
+
The package uses these tables (auto-created via Prisma migrations):
|
|
516
|
+
|
|
517
|
+
- **Site** - Your tracked sites/apps
|
|
518
|
+
- **PageView** - Individual page view events
|
|
519
|
+
- **Event** - Custom events
|
|
520
|
+
- **DailySalt** - Rotating salts for visitor hashing (privacy)
|
|
521
|
+
|
|
522
|
+
See `prisma/schema.prisma` for the full schema.
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Limitations
|
|
527
|
+
|
|
528
|
+
- **Vercel hosting required for geo data** - Location headers are provided by Vercel. Other hosts won't have geo data unless you add a third-party GeoIP service.
|
|
529
|
+
- **No local geo data** - When running locally, geo headers are not available. Location will be `null` in development.
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## License
|
|
534
|
+
|
|
535
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side error capture for nosework
|
|
3
|
+
*
|
|
4
|
+
* This is a lightweight (~2KB) script that captures unhandled errors
|
|
5
|
+
* and sends them to your tracking endpoint.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { initErrorTracking } from '@seneris/nosework/client/errors'
|
|
10
|
+
*
|
|
11
|
+
* initErrorTracking({
|
|
12
|
+
* endpoint: '/api/analytics/error',
|
|
13
|
+
* siteId: process.env.NEXT_PUBLIC_ANALYTICS_SITE_ID!,
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export interface ErrorTrackingConfig {
|
|
18
|
+
/** The endpoint to send errors to (e.g., '/api/analytics/error') */
|
|
19
|
+
endpoint: string;
|
|
20
|
+
/** Your site ID for analytics */
|
|
21
|
+
siteId: string;
|
|
22
|
+
/** Optional: custom metadata to include with every error */
|
|
23
|
+
metadata?: Record<string, unknown>;
|
|
24
|
+
/** Optional: function to filter errors (return false to skip) */
|
|
25
|
+
filter?: (error: CapturedError) => boolean;
|
|
26
|
+
/** Optional: max errors to send per minute (default: 10) */
|
|
27
|
+
rateLimit?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface CapturedError {
|
|
30
|
+
message: string;
|
|
31
|
+
stack?: string;
|
|
32
|
+
url: string;
|
|
33
|
+
type: "error" | "unhandledrejection";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Initialize error tracking
|
|
37
|
+
* Call this once when your app starts
|
|
38
|
+
*/
|
|
39
|
+
export declare function initErrorTracking(options: ErrorTrackingConfig): void;
|
|
40
|
+
/**
|
|
41
|
+
* Stop error tracking and remove event listeners
|
|
42
|
+
*/
|
|
43
|
+
export declare function stopErrorTracking(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Manually track an error
|
|
46
|
+
* Useful for caught exceptions you still want to track
|
|
47
|
+
*/
|
|
48
|
+
export declare function captureError(error: Error | string, metadata?: Record<string, unknown>): void;
|
|
49
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/client/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,iEAAiE;IACjE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,OAAO,CAAC;IAC3C,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,OAAO,GAAG,oBAAoB,CAAC;CACtC;AAsHD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAWpE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAMxC;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,KAAK,GAAG,MAAM,EACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,IAAI,CAiCN"}
|