@seneris/nosework 0.1.0 → 0.2.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 +28 -44
- package/dist/client.d.ts +9 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +26 -17
- package/dist/client.js.map +1 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +123 -117
- package/dist/error.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/query.d.ts +0 -11
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +173 -212
- package/dist/query.js.map +1 -1
- package/dist/schema.d.ts +979 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +101 -0
- package/dist/schema.js.map +1 -0
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +27 -30
- package/dist/track.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +31 -22
- package/dist/utils.js.map +1 -1
- package/package.json +4 -9
- package/prisma/schema.prisma +0 -145
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Privacy-focused, self-hosted analytics for your app suite.
|
|
|
19
19
|
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
|
20
20
|
│ │ │
|
|
21
21
|
│ import { trackPageView } │
|
|
22
|
-
│ from 'nosework'
|
|
22
|
+
│ from '@seneris/nosework' │
|
|
23
23
|
│ │ │
|
|
24
24
|
└────────────────┼────────────────┘
|
|
25
25
|
│
|
|
@@ -81,7 +81,7 @@ If you're not using Vercel, you have options:
|
|
|
81
81
|
### 1. Install
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
bun add nosework
|
|
84
|
+
bun add @seneris/nosework
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
### 2. Set Environment Variables
|
|
@@ -89,26 +89,23 @@ bun add nosework
|
|
|
89
89
|
Add to your `.env`:
|
|
90
90
|
|
|
91
91
|
```env
|
|
92
|
-
|
|
92
|
+
ANALYTICS_DATABASE_URL="postgresql://user:pass@host/dbname"
|
|
93
93
|
ANALYTICS_SITE_ID="my-app" # Unique identifier for this app
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
**MoopySuite Users:**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
First time only (or when updating nosework):
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
bunx prisma migrate deploy --schema=./node_modules/nosework/prisma/schema.prisma
|
|
96
|
+
**MoopySuite Users:** Your analytics tables are in the MoopySuite database, so use:
|
|
97
|
+
```env
|
|
98
|
+
ANALYTICS_DATABASE_URL="${DATABASE_URL}" # Same as MoopySuite DB
|
|
99
|
+
# No ANALYTICS_SITE_ID needed - use MOOPY_CLIENT_ID instead
|
|
104
100
|
```
|
|
101
|
+
See [MoopySuite Integration](./docs/MOOPYSUITE_INTEGRATION.md).
|
|
105
102
|
|
|
106
|
-
###
|
|
103
|
+
### 3. Add Tracking Endpoint
|
|
107
104
|
|
|
108
105
|
Create `app/api/analytics/track/route.ts`:
|
|
109
106
|
|
|
110
107
|
```typescript
|
|
111
|
-
import { trackPageView } from 'nosework';
|
|
108
|
+
import { trackPageView } from '@seneris/nosework';
|
|
112
109
|
import { NextRequest, NextResponse } from 'next/server';
|
|
113
110
|
|
|
114
111
|
export async function POST(request: NextRequest) {
|
|
@@ -137,7 +134,7 @@ export async function POST(request: NextRequest) {
|
|
|
137
134
|
}
|
|
138
135
|
```
|
|
139
136
|
|
|
140
|
-
###
|
|
137
|
+
### 4. Add Client Component
|
|
141
138
|
|
|
142
139
|
Create `components/Analytics.tsx`:
|
|
143
140
|
|
|
@@ -177,7 +174,7 @@ export function Analytics() {
|
|
|
177
174
|
}
|
|
178
175
|
```
|
|
179
176
|
|
|
180
|
-
###
|
|
177
|
+
### 5. Add to Layout
|
|
181
178
|
|
|
182
179
|
In `app/layout.tsx`:
|
|
183
180
|
|
|
@@ -210,7 +207,7 @@ If you prefer not to use a client component, you can track in Next.js middleware
|
|
|
210
207
|
Create `middleware.ts`:
|
|
211
208
|
|
|
212
209
|
```typescript
|
|
213
|
-
import { trackPageView } from 'nosework';
|
|
210
|
+
import { trackPageView } from '@seneris/nosework';
|
|
214
211
|
import { NextResponse } from 'next/server';
|
|
215
212
|
import type { NextRequest } from 'next/server';
|
|
216
213
|
|
|
@@ -261,7 +258,7 @@ export const config = {
|
|
|
261
258
|
Beyond page views, you can track custom events:
|
|
262
259
|
|
|
263
260
|
```typescript
|
|
264
|
-
import { trackEvent } from 'nosework';
|
|
261
|
+
import { trackEvent } from '@seneris/nosework';
|
|
265
262
|
|
|
266
263
|
// In an API route or server action
|
|
267
264
|
await trackEvent({
|
|
@@ -289,7 +286,7 @@ Common events to track:
|
|
|
289
286
|
### Basic Stats
|
|
290
287
|
|
|
291
288
|
```typescript
|
|
292
|
-
import { getStats } from 'nosework';
|
|
289
|
+
import { getStats } from '@seneris/nosework';
|
|
293
290
|
|
|
294
291
|
const stats = await getStats({
|
|
295
292
|
siteId: 'my-app',
|
|
@@ -309,7 +306,7 @@ const stats = await getStats({
|
|
|
309
306
|
### Top Pages
|
|
310
307
|
|
|
311
308
|
```typescript
|
|
312
|
-
import { getTopPages } from 'nosework';
|
|
309
|
+
import { getTopPages } from '@seneris/nosework';
|
|
313
310
|
|
|
314
311
|
const pages = await getTopPages({
|
|
315
312
|
siteId: 'my-app',
|
|
@@ -329,7 +326,7 @@ const pages = await getTopPages({
|
|
|
329
326
|
### Location Breakdown
|
|
330
327
|
|
|
331
328
|
```typescript
|
|
332
|
-
import { getLocations } from 'nosework';
|
|
329
|
+
import { getLocations } from '@seneris/nosework';
|
|
333
330
|
|
|
334
331
|
const locations = await getLocations({
|
|
335
332
|
siteId: 'my-app',
|
|
@@ -349,7 +346,7 @@ const locations = await getLocations({
|
|
|
349
346
|
### Traffic Sources
|
|
350
347
|
|
|
351
348
|
```typescript
|
|
352
|
-
import { getReferrers } from 'nosework';
|
|
349
|
+
import { getReferrers } from '@seneris/nosework';
|
|
353
350
|
|
|
354
351
|
const referrers = await getReferrers({
|
|
355
352
|
siteId: 'my-app',
|
|
@@ -362,7 +359,7 @@ const referrers = await getReferrers({
|
|
|
362
359
|
### Device/Browser Breakdown
|
|
363
360
|
|
|
364
361
|
```typescript
|
|
365
|
-
import { getDevices } from 'nosework';
|
|
362
|
+
import { getDevices } from '@seneris/nosework';
|
|
366
363
|
|
|
367
364
|
const devices = await getDevices({
|
|
368
365
|
siteId: 'my-app',
|
|
@@ -374,7 +371,7 @@ const devices = await getDevices({
|
|
|
374
371
|
### Time Series (for charts)
|
|
375
372
|
|
|
376
373
|
```typescript
|
|
377
|
-
import { getTimeSeries } from 'nosework';
|
|
374
|
+
import { getTimeSeries } from '@seneris/nosework';
|
|
378
375
|
|
|
379
376
|
const data = await getTimeSeries({
|
|
380
377
|
siteId: 'my-app',
|
|
@@ -391,18 +388,6 @@ const data = await getTimeSeries({
|
|
|
391
388
|
// ]
|
|
392
389
|
```
|
|
393
390
|
|
|
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
391
|
---
|
|
407
392
|
|
|
408
393
|
## Privacy Design
|
|
@@ -495,14 +480,12 @@ interface PaginatedQueryOptions extends QueryOptions {
|
|
|
495
480
|
| `getReferrers(options)` | `[{ referrer, pageViews, visitors }]` |
|
|
496
481
|
| `getDevices(options)` | `[{ device, browser, os, pageViews, visitors }]` |
|
|
497
482
|
| `getTimeSeries(options)` | `[{ date, pageViews, visitors }]` |
|
|
498
|
-
| `listSites()` | `[{ id, name, domain, createdAt }]` |
|
|
499
|
-
| `getOrCreateSite(domain, name?)` | `{ id, name, domain }` |
|
|
500
483
|
|
|
501
484
|
### Utility Functions
|
|
502
485
|
|
|
503
486
|
| Function | Description |
|
|
504
487
|
|----------|-------------|
|
|
505
|
-
| `getClient()` | Get the
|
|
488
|
+
| `getClient()` | Get the Drizzle client for custom queries |
|
|
506
489
|
| `disconnect()` | Disconnect from the database |
|
|
507
490
|
| `isBot(userAgent)` | Check if a user-agent is a bot |
|
|
508
491
|
| `parseUserAgent(ua)` | Parse a user-agent string |
|
|
@@ -512,14 +495,15 @@ interface PaginatedQueryOptions extends QueryOptions {
|
|
|
512
495
|
|
|
513
496
|
## Database Schema
|
|
514
497
|
|
|
515
|
-
The package uses these tables (
|
|
498
|
+
The package uses these tables (created by MoopySuite migrations):
|
|
516
499
|
|
|
517
|
-
- **
|
|
518
|
-
- **
|
|
519
|
-
- **
|
|
520
|
-
- **
|
|
500
|
+
- **page_views** - Individual page view events with location, device info
|
|
501
|
+
- **events** - Custom events with properties
|
|
502
|
+
- **daily_salts** - Rotating salts for visitor hashing (privacy)
|
|
503
|
+
- **analytics_errors** - Individual error occurrences
|
|
504
|
+
- **error_groups** - Aggregated errors by fingerprint
|
|
521
505
|
|
|
522
|
-
See `
|
|
506
|
+
See `src/schema.ts` for the Drizzle schema definitions.
|
|
523
507
|
|
|
524
508
|
---
|
|
525
509
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
2
|
+
import postgres from "postgres";
|
|
3
|
+
import * as schema from "./schema.js";
|
|
2
4
|
declare global {
|
|
3
|
-
var
|
|
5
|
+
var __noseworkDb: ReturnType<typeof drizzle<typeof schema>> | undefined;
|
|
6
|
+
var __noseworkSql: ReturnType<typeof postgres> | undefined;
|
|
4
7
|
}
|
|
5
|
-
export declare function getClient():
|
|
8
|
+
export declare function getClient(): import("drizzle-orm/postgres-js").PostgresJsDatabase<typeof schema> & {
|
|
9
|
+
$client: postgres.Sql<{}>;
|
|
10
|
+
};
|
|
6
11
|
export declare function disconnect(): Promise<void>;
|
|
12
|
+
export { schema };
|
|
7
13
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,QAAQ,MAAM,UAAU,CAAC;AAChC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,YAAY,EAAE,UAAU,CAAC,OAAO,OAAO,CAAC,OAAO,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAExE,IAAI,aAAa,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,GAAG,SAAS,CAAC;CAC5D;AAKD,wBAAgB,SAAS;;EAwBxB;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAMhD;AAGD,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
package/dist/client.js
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
2
|
+
import postgres from "postgres";
|
|
3
|
+
import * as schema from "./schema.js";
|
|
4
|
+
let db = null;
|
|
5
|
+
let sql = null;
|
|
3
6
|
export function getClient() {
|
|
4
|
-
if (
|
|
5
|
-
return
|
|
7
|
+
if (db) {
|
|
8
|
+
return db;
|
|
6
9
|
}
|
|
7
|
-
|
|
10
|
+
const connectionString = process.env.ANALYTICS_DATABASE_URL;
|
|
11
|
+
if (!connectionString) {
|
|
12
|
+
throw new Error("ANALYTICS_DATABASE_URL environment variable is not set");
|
|
13
|
+
}
|
|
14
|
+
// In development, use global to preserve connection across hot reloads
|
|
8
15
|
if (process.env.NODE_ENV === "development") {
|
|
9
|
-
if (!global.
|
|
10
|
-
global.
|
|
11
|
-
|
|
12
|
-
});
|
|
16
|
+
if (!global.__noseworkSql) {
|
|
17
|
+
global.__noseworkSql = postgres(connectionString);
|
|
18
|
+
global.__noseworkDb = drizzle(global.__noseworkSql, { schema });
|
|
13
19
|
}
|
|
14
|
-
|
|
20
|
+
sql = global.__noseworkSql;
|
|
21
|
+
db = global.__noseworkDb;
|
|
15
22
|
}
|
|
16
23
|
else {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
});
|
|
24
|
+
sql = postgres(connectionString);
|
|
25
|
+
db = drizzle(sql, { schema });
|
|
20
26
|
}
|
|
21
|
-
return
|
|
27
|
+
return db;
|
|
22
28
|
}
|
|
23
29
|
export async function disconnect() {
|
|
24
|
-
if (
|
|
25
|
-
await
|
|
26
|
-
|
|
30
|
+
if (sql) {
|
|
31
|
+
await sql.end();
|
|
32
|
+
sql = null;
|
|
33
|
+
db = null;
|
|
27
34
|
}
|
|
28
35
|
}
|
|
36
|
+
// Re-export schema for convenience
|
|
37
|
+
export { schema };
|
|
29
38
|
//# sourceMappingURL=client.js.map
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,QAAQ,MAAM,UAAU,CAAC;AAChC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAStC,IAAI,EAAE,GAAqD,IAAI,CAAC;AAChE,IAAI,GAAG,GAAuC,IAAI,CAAC;AAEnD,MAAM,UAAU,SAAS;IACvB,IAAI,EAAE,EAAE,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC;IAC5D,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC5E,CAAC;IAED,uEAAuE;IACvE,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;QAC3C,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;YAC1B,MAAM,CAAC,aAAa,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAAC;YAClD,MAAM,CAAC,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QAClE,CAAC;QACD,GAAG,GAAG,MAAM,CAAC,aAAa,CAAC;QAC3B,EAAE,GAAG,MAAM,CAAC,YAAa,CAAC;IAC5B,CAAC;SAAM,CAAC;QACN,GAAG,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAAC;QACjC,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAChC,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC;QAChB,GAAG,GAAG,IAAI,CAAC;QACX,EAAE,GAAG,IAAI,CAAC;IACZ,CAAC;AACH,CAAC;AAED,mCAAmC;AACnC,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
package/dist/error.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,aAAa,EACb,gBAAgB,EACjB,MAAM,YAAY,CAAC;AA8CpB;;GAEG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD1E;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,IAAI,CAAA;CAAE,GAC7C,OAAO,CAAC,UAAU,CAAC,CA4CrB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACA,OAAO,CAAC,cAAc,EAAE,CAAC,CA4B3B;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,aAAa,EAAE,CAAC,CA6B1B;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,IAAI,GACd,OAAO,CAAC;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC,CA0C3D"}
|
package/dist/error.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getClient } from "./client.js";
|
|
2
|
+
import { analyticsErrors, errorGroups } from "./schema.js";
|
|
3
|
+
import { eq, and, gte, lte, lt, count, countDistinct, desc, inArray } from "drizzle-orm";
|
|
2
4
|
import { createHash } from "crypto";
|
|
3
5
|
/**
|
|
4
6
|
* Create a fingerprint for grouping similar errors
|
|
@@ -16,14 +18,12 @@ function createFingerprint(message, stack) {
|
|
|
16
18
|
let topFrame = "";
|
|
17
19
|
if (stack) {
|
|
18
20
|
const lines = stack.split("\n");
|
|
19
|
-
// Find first line that looks like a stack frame (starts with "at " or similar)
|
|
20
21
|
for (const line of lines.slice(1)) {
|
|
21
22
|
const trimmed = line.trim();
|
|
22
23
|
if (trimmed.startsWith("at ") || trimmed.match(/^\w+@/)) {
|
|
23
|
-
// Normalize file paths and line numbers
|
|
24
24
|
topFrame = trimmed
|
|
25
|
-
.replace(/:\d+:\d+/g, ":L:C")
|
|
26
|
-
.replace(/\?.*/g, "");
|
|
25
|
+
.replace(/:\d+:\d+/g, ":L:C")
|
|
26
|
+
.replace(/\?.*/g, "");
|
|
27
27
|
break;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -52,89 +52,93 @@ export async function trackError(options) {
|
|
|
52
52
|
const pathname = extractPathname(options.url);
|
|
53
53
|
const now = new Date();
|
|
54
54
|
// Create the error record
|
|
55
|
-
await db.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
metadata: options.metadata ?? undefined,
|
|
71
|
-
},
|
|
55
|
+
await db.insert(analyticsErrors).values({
|
|
56
|
+
siteId: options.siteId,
|
|
57
|
+
message: options.message,
|
|
58
|
+
stack: options.stack ?? null,
|
|
59
|
+
fingerprint,
|
|
60
|
+
url: options.url,
|
|
61
|
+
pathname,
|
|
62
|
+
visitorHash: options.visitorHash ?? null,
|
|
63
|
+
sessionId: options.sessionId ?? null,
|
|
64
|
+
userId: options.userId ?? null,
|
|
65
|
+
browser: options.browser ?? null,
|
|
66
|
+
browserVer: options.browserVer ?? null,
|
|
67
|
+
os: options.os ?? null,
|
|
68
|
+
device: options.device ?? null,
|
|
69
|
+
metadata: options.metadata ?? null,
|
|
72
70
|
});
|
|
73
|
-
//
|
|
74
|
-
await db
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
// Check if error group exists
|
|
72
|
+
const [existingGroup] = await db
|
|
73
|
+
.select()
|
|
74
|
+
.from(errorGroups)
|
|
75
|
+
.where(and(eq(errorGroups.siteId, options.siteId), eq(errorGroups.fingerprint, fingerprint)))
|
|
76
|
+
.limit(1);
|
|
77
|
+
if (existingGroup) {
|
|
78
|
+
// Update existing group
|
|
79
|
+
await db
|
|
80
|
+
.update(errorGroups)
|
|
81
|
+
.set({
|
|
82
|
+
count: existingGroup.count + 1,
|
|
83
|
+
lastSeen: now,
|
|
84
|
+
...(options.stack && { stack: options.stack }),
|
|
85
|
+
})
|
|
86
|
+
.where(eq(errorGroups.id, existingGroup.id));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Create new group
|
|
90
|
+
await db.insert(errorGroups).values({
|
|
82
91
|
siteId: options.siteId,
|
|
83
92
|
fingerprint,
|
|
84
93
|
message: options.message,
|
|
85
|
-
stack: options.stack,
|
|
94
|
+
stack: options.stack ?? null,
|
|
86
95
|
count: 1,
|
|
87
96
|
firstSeen: now,
|
|
88
97
|
lastSeen: now,
|
|
89
98
|
status: "open",
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
count: { increment: 1 },
|
|
93
|
-
lastSeen: now,
|
|
94
|
-
// Update message/stack if this is a clearer example
|
|
95
|
-
...(options.stack && { stack: options.stack }),
|
|
96
|
-
},
|
|
97
|
-
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
98
101
|
}
|
|
99
102
|
/**
|
|
100
103
|
* Get error statistics
|
|
101
104
|
*/
|
|
102
105
|
export async function getErrorStats(siteId, options) {
|
|
103
106
|
const db = getClient();
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
107
|
+
// Build where conditions
|
|
108
|
+
const whereConditions = [eq(analyticsErrors.siteId, siteId)];
|
|
109
|
+
if (options?.startDate) {
|
|
110
|
+
whereConditions.push(gte(analyticsErrors.timestamp, options.startDate));
|
|
111
|
+
}
|
|
112
|
+
if (options?.endDate) {
|
|
113
|
+
whereConditions.push(lte(analyticsErrors.timestamp, options.endDate));
|
|
114
|
+
}
|
|
115
|
+
const where = and(...whereConditions);
|
|
116
|
+
// Total errors
|
|
117
|
+
const [totalResult] = await db
|
|
118
|
+
.select({ count: count() })
|
|
119
|
+
.from(analyticsErrors)
|
|
120
|
+
.where(where);
|
|
121
|
+
// Unique errors (by fingerprint)
|
|
122
|
+
const [uniqueResult] = await db
|
|
123
|
+
.select({ count: countDistinct(analyticsErrors.fingerprint) })
|
|
124
|
+
.from(analyticsErrors)
|
|
125
|
+
.where(where);
|
|
121
126
|
// Errors in last 24 hours
|
|
122
127
|
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
},
|
|
128
|
-
});
|
|
128
|
+
const [todayResult] = await db
|
|
129
|
+
.select({ count: count() })
|
|
130
|
+
.from(analyticsErrors)
|
|
131
|
+
.where(and(eq(analyticsErrors.siteId, siteId), gte(analyticsErrors.timestamp, oneDayAgo)));
|
|
129
132
|
// Open error groups
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
const [openResult] = await db
|
|
134
|
+
.select({ count: count() })
|
|
135
|
+
.from(errorGroups)
|
|
136
|
+
.where(and(eq(errorGroups.siteId, siteId), eq(errorGroups.status, "open")));
|
|
133
137
|
return {
|
|
134
|
-
totalErrors,
|
|
135
|
-
uniqueErrors,
|
|
136
|
-
errorsToday,
|
|
137
|
-
openGroups,
|
|
138
|
+
totalErrors: totalResult?.count ?? 0,
|
|
139
|
+
uniqueErrors: uniqueResult?.count ?? 0,
|
|
140
|
+
errorsToday: todayResult?.count ?? 0,
|
|
141
|
+
openGroups: openResult?.count ?? 0,
|
|
138
142
|
};
|
|
139
143
|
}
|
|
140
144
|
/**
|
|
@@ -144,15 +148,17 @@ export async function getErrorGroups(siteId, options) {
|
|
|
144
148
|
const db = getClient();
|
|
145
149
|
const limit = options?.limit ?? 50;
|
|
146
150
|
const offset = options?.offset ?? 0;
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
151
|
+
const whereConditions = [eq(errorGroups.siteId, siteId)];
|
|
152
|
+
if (options?.status) {
|
|
153
|
+
whereConditions.push(eq(errorGroups.status, options.status));
|
|
154
|
+
}
|
|
155
|
+
const groups = await db
|
|
156
|
+
.select()
|
|
157
|
+
.from(errorGroups)
|
|
158
|
+
.where(and(...whereConditions))
|
|
159
|
+
.orderBy(desc(errorGroups.lastSeen))
|
|
160
|
+
.limit(limit)
|
|
161
|
+
.offset(offset);
|
|
156
162
|
return groups.map((g) => ({
|
|
157
163
|
id: g.id,
|
|
158
164
|
fingerprint: g.fingerprint,
|
|
@@ -171,12 +177,13 @@ export async function getErrorInstances(siteId, fingerprint, options) {
|
|
|
171
177
|
const db = getClient();
|
|
172
178
|
const limit = options?.limit ?? 50;
|
|
173
179
|
const offset = options?.offset ?? 0;
|
|
174
|
-
const errors = await db
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
+
const errors = await db
|
|
181
|
+
.select()
|
|
182
|
+
.from(analyticsErrors)
|
|
183
|
+
.where(and(eq(analyticsErrors.siteId, siteId), eq(analyticsErrors.fingerprint, fingerprint)))
|
|
184
|
+
.orderBy(desc(analyticsErrors.timestamp))
|
|
185
|
+
.limit(limit)
|
|
186
|
+
.offset(offset);
|
|
180
187
|
return errors.map((e) => ({
|
|
181
188
|
id: e.id,
|
|
182
189
|
message: e.message,
|
|
@@ -199,12 +206,10 @@ export async function getErrorInstances(siteId, fingerprint, options) {
|
|
|
199
206
|
*/
|
|
200
207
|
export async function updateErrorGroupStatus(siteId, fingerprint, status) {
|
|
201
208
|
const db = getClient();
|
|
202
|
-
await db
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
data: { status },
|
|
207
|
-
});
|
|
209
|
+
await db
|
|
210
|
+
.update(errorGroups)
|
|
211
|
+
.set({ status })
|
|
212
|
+
.where(and(eq(errorGroups.siteId, siteId), eq(errorGroups.fingerprint, fingerprint)));
|
|
208
213
|
}
|
|
209
214
|
/**
|
|
210
215
|
* Delete errors older than a certain date
|
|
@@ -213,36 +218,37 @@ export async function updateErrorGroupStatus(siteId, fingerprint, status) {
|
|
|
213
218
|
export async function deleteOldErrors(siteId, olderThan) {
|
|
214
219
|
const db = getClient();
|
|
215
220
|
// Delete old error instances
|
|
216
|
-
const deletedErrors = await db
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
where: { siteId },
|
|
227
|
-
});
|
|
221
|
+
const deletedErrors = await db
|
|
222
|
+
.delete(analyticsErrors)
|
|
223
|
+
.where(and(eq(analyticsErrors.siteId, siteId), lt(analyticsErrors.timestamp, olderThan)))
|
|
224
|
+
.returning({ id: analyticsErrors.id });
|
|
225
|
+
// Get fingerprints that still have errors
|
|
226
|
+
const remainingFingerprints = await db
|
|
227
|
+
.select({ fingerprint: analyticsErrors.fingerprint })
|
|
228
|
+
.from(analyticsErrors)
|
|
229
|
+
.where(eq(analyticsErrors.siteId, siteId))
|
|
230
|
+
.groupBy(analyticsErrors.fingerprint);
|
|
228
231
|
const activeFingerprints = new Set(remainingFingerprints.map((r) => r.fingerprint));
|
|
229
|
-
//
|
|
230
|
-
const allGroups = await db
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
// Get all groups for this site
|
|
233
|
+
const allGroups = await db
|
|
234
|
+
.select({ id: errorGroups.id, fingerprint: errorGroups.fingerprint })
|
|
235
|
+
.from(errorGroups)
|
|
236
|
+
.where(eq(errorGroups.siteId, siteId));
|
|
237
|
+
// Find groups to delete
|
|
238
|
+
const groupIdsToDelete = allGroups
|
|
235
239
|
.filter((g) => !activeFingerprints.has(g.fingerprint))
|
|
236
|
-
.map((g) => g.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
.map((g) => g.id);
|
|
241
|
+
let deletedGroupCount = 0;
|
|
242
|
+
if (groupIdsToDelete.length > 0) {
|
|
243
|
+
const deletedGroups = await db
|
|
244
|
+
.delete(errorGroups)
|
|
245
|
+
.where(inArray(errorGroups.id, groupIdsToDelete))
|
|
246
|
+
.returning({ id: errorGroups.id });
|
|
247
|
+
deletedGroupCount = deletedGroups.length;
|
|
248
|
+
}
|
|
243
249
|
return {
|
|
244
|
-
deletedErrors: deletedErrors.
|
|
245
|
-
deletedGroups:
|
|
250
|
+
deletedErrors: deletedErrors.length,
|
|
251
|
+
deletedGroups: deletedGroupCount,
|
|
246
252
|
};
|
|
247
253
|
}
|
|
248
254
|
//# sourceMappingURL=error.js.map
|