@litemetrics/node 0.1.0 → 0.1.1
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 +108 -0
- package/dist/index.cjs +123 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +121 -26
- package/dist/index.js.map +1 -1
- package/package.json +12 -3
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @litemetrics/node
|
|
2
|
+
|
|
3
|
+
Self-hosted analytics server for Litemetrics. Includes event collection, query API, site management, GeoIP enrichment, and bot filtering.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @litemetrics/node
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import express from 'express';
|
|
15
|
+
import { createCollector } from '@litemetrics/node';
|
|
16
|
+
|
|
17
|
+
const app = express();
|
|
18
|
+
app.use(express.json());
|
|
19
|
+
|
|
20
|
+
const collector = await createCollector({
|
|
21
|
+
db: { url: 'http://localhost:8123' }, // ClickHouse (default)
|
|
22
|
+
adminSecret: 'your-admin-secret',
|
|
23
|
+
geoip: true,
|
|
24
|
+
cors: { origins: [] }, // Allow all origins
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Event collection endpoint (receives tracker data)
|
|
28
|
+
app.post('/api/collect', collector.handler());
|
|
29
|
+
|
|
30
|
+
// Query API (stats, time series, retention)
|
|
31
|
+
app.get('/api/stats', collector.queryHandler());
|
|
32
|
+
|
|
33
|
+
// Event & user listing
|
|
34
|
+
app.all('/api/events', collector.eventsHandler());
|
|
35
|
+
app.all('/api/users/*', collector.usersHandler());
|
|
36
|
+
|
|
37
|
+
// Site management (CRUD)
|
|
38
|
+
app.all('/api/sites/*', collector.sitesHandler());
|
|
39
|
+
|
|
40
|
+
// Serve tracker script
|
|
41
|
+
app.use(express.static('node_modules/@litemetrics/tracker/dist'));
|
|
42
|
+
|
|
43
|
+
app.listen(3000);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Database Adapters
|
|
47
|
+
|
|
48
|
+
### ClickHouse (Default)
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const collector = await createCollector({
|
|
52
|
+
db: { url: 'http://localhost:8123' },
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Uses `MergeTree` for events and `ReplacingMergeTree` for sites. Tables are auto-created on init.
|
|
57
|
+
|
|
58
|
+
### MongoDB
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const collector = await createCollector({
|
|
62
|
+
db: { adapter: 'mongodb', url: 'mongodb://localhost:27017/litemetrics' },
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Features
|
|
67
|
+
|
|
68
|
+
- **Event Collection** - Receives batched events from the browser tracker
|
|
69
|
+
- **Bot Filtering** - Automatically drops events from known bots and crawlers
|
|
70
|
+
- **GeoIP Enrichment** - Resolves country/city from IP using MaxMind GeoLite2
|
|
71
|
+
- **User-Agent Parsing** - Extracts browser, OS, and device type
|
|
72
|
+
- **Hostname Filtering** - Only count events from allowed hostnames per site
|
|
73
|
+
- **Query API** - 12 built-in metrics (pageviews, visitors, sessions, top pages, etc.)
|
|
74
|
+
- **Time Series** - Hourly/daily/weekly/monthly breakdowns
|
|
75
|
+
- **Retention Analysis** - Weekly cohort retention
|
|
76
|
+
- **Site Management** - Multi-tenant CRUD with secret key auth
|
|
77
|
+
- **Server-Side Tracking** - Track events and identify users from your backend
|
|
78
|
+
|
|
79
|
+
## Server-Side Tracking
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// Track events from your backend
|
|
83
|
+
await collector.track('site-id', 'Purchase', { amount: 99 }, { userId: 'user-123' });
|
|
84
|
+
|
|
85
|
+
// Identify users
|
|
86
|
+
await collector.identify('site-id', 'user-123', { plan: 'pro' });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Metrics
|
|
90
|
+
|
|
91
|
+
| Metric | Description |
|
|
92
|
+
|--------|-------------|
|
|
93
|
+
| `pageviews` | Total page views |
|
|
94
|
+
| `visitors` | Unique visitors |
|
|
95
|
+
| `sessions` | Unique sessions |
|
|
96
|
+
| `events` | Custom events count |
|
|
97
|
+
| `top_pages` | Most visited pages |
|
|
98
|
+
| `top_referrers` | Top traffic sources |
|
|
99
|
+
| `top_countries` | Visitors by country |
|
|
100
|
+
| `top_cities` | Visitors by city |
|
|
101
|
+
| `top_events` | Most common custom events |
|
|
102
|
+
| `top_browsers` | Browser breakdown |
|
|
103
|
+
| `top_os` | OS breakdown |
|
|
104
|
+
| `top_devices` | Device type breakdown |
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -32,7 +32,8 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
ClickHouseAdapter: () => ClickHouseAdapter,
|
|
34
34
|
MongoDBAdapter: () => MongoDBAdapter,
|
|
35
|
-
createCollector: () => createCollector
|
|
35
|
+
createCollector: () => createCollector,
|
|
36
|
+
isBot: () => isBot
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(index_exports);
|
|
38
39
|
|
|
@@ -230,6 +231,10 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
230
231
|
ORDER BY (site_id)
|
|
231
232
|
SETTINGS index_granularity = 8192
|
|
232
233
|
`;
|
|
234
|
+
function toCHDateTime(d) {
|
|
235
|
+
const iso = typeof d === "string" ? d : d.toISOString();
|
|
236
|
+
return iso.replace("T", " ").replace("Z", "");
|
|
237
|
+
}
|
|
233
238
|
var ClickHouseAdapter = class {
|
|
234
239
|
client;
|
|
235
240
|
constructor(url) {
|
|
@@ -253,7 +258,7 @@ var ClickHouseAdapter = class {
|
|
|
253
258
|
const rows = events.map((e) => ({
|
|
254
259
|
site_id: e.siteId,
|
|
255
260
|
type: e.type,
|
|
256
|
-
timestamp: new Date(e.timestamp)
|
|
261
|
+
timestamp: toCHDateTime(new Date(e.timestamp)),
|
|
257
262
|
session_id: e.sessionId,
|
|
258
263
|
visitor_id: e.visitorId,
|
|
259
264
|
url: e.url ?? null,
|
|
@@ -293,8 +298,8 @@ var ClickHouseAdapter = class {
|
|
|
293
298
|
const limit = q.limit ?? 10;
|
|
294
299
|
const params = {
|
|
295
300
|
siteId,
|
|
296
|
-
from: dateRange.from,
|
|
297
|
-
to: dateRange.to,
|
|
301
|
+
from: toCHDateTime(dateRange.from),
|
|
302
|
+
to: toCHDateTime(dateRange.to),
|
|
298
303
|
limit
|
|
299
304
|
};
|
|
300
305
|
let data = [];
|
|
@@ -542,8 +547,8 @@ var ClickHouseAdapter = class {
|
|
|
542
547
|
}
|
|
543
548
|
const rows = await this.queryRows(sql, {
|
|
544
549
|
siteId: params.siteId,
|
|
545
|
-
from: dateRange.from,
|
|
546
|
-
to: dateRange.to
|
|
550
|
+
from: toCHDateTime(dateRange.from),
|
|
551
|
+
to: toCHDateTime(dateRange.to)
|
|
547
552
|
});
|
|
548
553
|
const mappedRows = rows.map((r) => ({
|
|
549
554
|
_id: this.convertClickHouseBucket(r.bucket, granularity),
|
|
@@ -609,7 +614,7 @@ var ClickHouseAdapter = class {
|
|
|
609
614
|
GROUP BY visitor_id`,
|
|
610
615
|
{
|
|
611
616
|
siteId: params.siteId,
|
|
612
|
-
since: startDate
|
|
617
|
+
since: toCHDateTime(startDate)
|
|
613
618
|
}
|
|
614
619
|
);
|
|
615
620
|
const cohortMap = /* @__PURE__ */ new Map();
|
|
@@ -676,8 +681,8 @@ var ClickHouseAdapter = class {
|
|
|
676
681
|
dateTo: params.dateTo
|
|
677
682
|
});
|
|
678
683
|
conditions.push(`timestamp >= {from:String} AND timestamp <= {to:String}`);
|
|
679
|
-
queryParams.from = dateRange.from;
|
|
680
|
-
queryParams.to = dateRange.to;
|
|
684
|
+
queryParams.from = toCHDateTime(dateRange.from);
|
|
685
|
+
queryParams.to = toCHDateTime(dateRange.to);
|
|
681
686
|
}
|
|
682
687
|
const where = conditions.join(" AND ");
|
|
683
688
|
const [events, countRows] = await Promise.all([
|
|
@@ -783,15 +788,17 @@ var ClickHouseAdapter = class {
|
|
|
783
788
|
}
|
|
784
789
|
// ─── Site Management ──────────────────────────────────────
|
|
785
790
|
async createSite(data) {
|
|
786
|
-
const now =
|
|
791
|
+
const now = /* @__PURE__ */ new Date();
|
|
792
|
+
const nowISO = now.toISOString();
|
|
793
|
+
const nowCH = toCHDateTime(now);
|
|
787
794
|
const site = {
|
|
788
795
|
siteId: generateSiteId(),
|
|
789
796
|
secretKey: generateSecretKey(),
|
|
790
797
|
name: data.name,
|
|
791
798
|
domain: data.domain,
|
|
792
799
|
allowedOrigins: data.allowedOrigins,
|
|
793
|
-
createdAt:
|
|
794
|
-
updatedAt:
|
|
800
|
+
createdAt: nowISO,
|
|
801
|
+
updatedAt: nowISO
|
|
795
802
|
};
|
|
796
803
|
await this.client.insert({
|
|
797
804
|
table: SITES_TABLE,
|
|
@@ -801,8 +808,8 @@ var ClickHouseAdapter = class {
|
|
|
801
808
|
name: site.name,
|
|
802
809
|
domain: site.domain ?? null,
|
|
803
810
|
allowed_origins: site.allowedOrigins ? JSON.stringify(site.allowedOrigins) : null,
|
|
804
|
-
created_at:
|
|
805
|
-
updated_at:
|
|
811
|
+
created_at: nowCH,
|
|
812
|
+
updated_at: nowCH,
|
|
806
813
|
version: 1,
|
|
807
814
|
is_deleted: 0
|
|
808
815
|
}],
|
|
@@ -847,7 +854,9 @@ var ClickHouseAdapter = class {
|
|
|
847
854
|
);
|
|
848
855
|
if (currentRows.length === 0) return null;
|
|
849
856
|
const current = currentRows[0];
|
|
850
|
-
const now =
|
|
857
|
+
const now = /* @__PURE__ */ new Date();
|
|
858
|
+
const nowISO = now.toISOString();
|
|
859
|
+
const nowCH = toCHDateTime(now);
|
|
851
860
|
const newVersion = Number(current.version) + 1;
|
|
852
861
|
const newName = data.name !== void 0 ? data.name : String(current.name);
|
|
853
862
|
const newDomain = data.domain !== void 0 ? data.domain || null : current.domain ? String(current.domain) : null;
|
|
@@ -860,8 +869,8 @@ var ClickHouseAdapter = class {
|
|
|
860
869
|
name: newName,
|
|
861
870
|
domain: newDomain,
|
|
862
871
|
allowed_origins: newOrigins,
|
|
863
|
-
created_at: String(current.created_at),
|
|
864
|
-
updated_at:
|
|
872
|
+
created_at: toCHDateTime(String(current.created_at)),
|
|
873
|
+
updated_at: nowCH,
|
|
865
874
|
version: newVersion,
|
|
866
875
|
is_deleted: 0
|
|
867
876
|
}],
|
|
@@ -874,7 +883,7 @@ var ClickHouseAdapter = class {
|
|
|
874
883
|
domain: newDomain ?? void 0,
|
|
875
884
|
allowedOrigins: newOrigins ? JSON.parse(newOrigins) : void 0,
|
|
876
885
|
createdAt: String(current.created_at),
|
|
877
|
-
updatedAt:
|
|
886
|
+
updatedAt: nowISO
|
|
878
887
|
};
|
|
879
888
|
}
|
|
880
889
|
async deleteSite(siteId) {
|
|
@@ -886,7 +895,7 @@ var ClickHouseAdapter = class {
|
|
|
886
895
|
);
|
|
887
896
|
if (currentRows.length === 0) return false;
|
|
888
897
|
const current = currentRows[0];
|
|
889
|
-
const
|
|
898
|
+
const nowCH = toCHDateTime(/* @__PURE__ */ new Date());
|
|
890
899
|
await this.client.insert({
|
|
891
900
|
table: SITES_TABLE,
|
|
892
901
|
values: [{
|
|
@@ -895,8 +904,8 @@ var ClickHouseAdapter = class {
|
|
|
895
904
|
name: String(current.name),
|
|
896
905
|
domain: current.domain ? String(current.domain) : null,
|
|
897
906
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
898
|
-
created_at: String(current.created_at),
|
|
899
|
-
updated_at:
|
|
907
|
+
created_at: toCHDateTime(String(current.created_at)),
|
|
908
|
+
updated_at: nowCH,
|
|
900
909
|
version: Number(current.version) + 1,
|
|
901
910
|
is_deleted: 1
|
|
902
911
|
}],
|
|
@@ -913,7 +922,9 @@ var ClickHouseAdapter = class {
|
|
|
913
922
|
);
|
|
914
923
|
if (currentRows.length === 0) return null;
|
|
915
924
|
const current = currentRows[0];
|
|
916
|
-
const now =
|
|
925
|
+
const now = /* @__PURE__ */ new Date();
|
|
926
|
+
const nowISO = now.toISOString();
|
|
927
|
+
const nowCH = toCHDateTime(now);
|
|
917
928
|
const newSecret = generateSecretKey();
|
|
918
929
|
await this.client.insert({
|
|
919
930
|
table: SITES_TABLE,
|
|
@@ -923,8 +934,8 @@ var ClickHouseAdapter = class {
|
|
|
923
934
|
name: String(current.name),
|
|
924
935
|
domain: current.domain ? String(current.domain) : null,
|
|
925
936
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
926
|
-
created_at: String(current.created_at),
|
|
927
|
-
updated_at:
|
|
937
|
+
created_at: toCHDateTime(String(current.created_at)),
|
|
938
|
+
updated_at: nowCH,
|
|
928
939
|
version: Number(current.version) + 1,
|
|
929
940
|
is_deleted: 0
|
|
930
941
|
}],
|
|
@@ -937,7 +948,7 @@ var ClickHouseAdapter = class {
|
|
|
937
948
|
domain: current.domain ? String(current.domain) : void 0,
|
|
938
949
|
allowedOrigins: current.allowed_origins ? JSON.parse(String(current.allowed_origins)) : void 0,
|
|
939
950
|
createdAt: String(current.created_at),
|
|
940
|
-
updatedAt:
|
|
951
|
+
updatedAt: nowISO
|
|
941
952
|
};
|
|
942
953
|
}
|
|
943
954
|
// ─── Helpers ─────────────────────────────────────────────
|
|
@@ -1700,6 +1711,63 @@ function resolveDeviceType(type) {
|
|
|
1700
1711
|
return "desktop";
|
|
1701
1712
|
}
|
|
1702
1713
|
|
|
1714
|
+
// src/botfilter.ts
|
|
1715
|
+
var BOT_PATTERNS = [
|
|
1716
|
+
// Headless browsers
|
|
1717
|
+
/HeadlessChrome/i,
|
|
1718
|
+
/PhantomJS/i,
|
|
1719
|
+
/Selenium/i,
|
|
1720
|
+
/Puppeteer/i,
|
|
1721
|
+
/Playwright/i,
|
|
1722
|
+
// Common bots
|
|
1723
|
+
/bot\b/i,
|
|
1724
|
+
/spider/i,
|
|
1725
|
+
/crawl/i,
|
|
1726
|
+
/slurp/i,
|
|
1727
|
+
/mediapartners/i,
|
|
1728
|
+
/facebookexternalhit/i,
|
|
1729
|
+
/Twitterbot/i,
|
|
1730
|
+
/LinkedInBot/i,
|
|
1731
|
+
/WhatsApp/i,
|
|
1732
|
+
/Discordbot/i,
|
|
1733
|
+
/TelegramBot/i,
|
|
1734
|
+
/Applebot/i,
|
|
1735
|
+
/Baiduspider/i,
|
|
1736
|
+
/YandexBot/i,
|
|
1737
|
+
/DuckDuckBot/i,
|
|
1738
|
+
/Sogou/i,
|
|
1739
|
+
/Exabot/i,
|
|
1740
|
+
/ia_archiver/i,
|
|
1741
|
+
// HTTP libraries & API tools
|
|
1742
|
+
/PostmanRuntime/i,
|
|
1743
|
+
/axios/i,
|
|
1744
|
+
/node-fetch/i,
|
|
1745
|
+
/python-requests/i,
|
|
1746
|
+
/Go-http-client/i,
|
|
1747
|
+
/Java\//i,
|
|
1748
|
+
/libwww-perl/i,
|
|
1749
|
+
/wget/i,
|
|
1750
|
+
/curl/i,
|
|
1751
|
+
/httpie/i,
|
|
1752
|
+
// Monitoring / uptime
|
|
1753
|
+
/UptimeRobot/i,
|
|
1754
|
+
/Pingdom/i,
|
|
1755
|
+
/StatusCake/i,
|
|
1756
|
+
/Site24x7/i,
|
|
1757
|
+
/NewRelic/i,
|
|
1758
|
+
/Datadog/i,
|
|
1759
|
+
// Preview/embed
|
|
1760
|
+
/Slackbot/i,
|
|
1761
|
+
/Embedly/i,
|
|
1762
|
+
/Quora Link Preview/i,
|
|
1763
|
+
/redditbot/i,
|
|
1764
|
+
/Pinterestbot/i
|
|
1765
|
+
];
|
|
1766
|
+
function isBot(ua) {
|
|
1767
|
+
if (!ua || ua.length === 0) return true;
|
|
1768
|
+
return BOT_PATTERNS.some((re) => re.test(ua));
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1703
1771
|
// src/collector.ts
|
|
1704
1772
|
async function createCollector(config) {
|
|
1705
1773
|
const db = createAdapter(config.db);
|
|
@@ -1772,9 +1840,36 @@ async function createCollector(config) {
|
|
|
1772
1840
|
sendJson(res, 400, { ok: false, error: "Too many events (max 100)" });
|
|
1773
1841
|
return;
|
|
1774
1842
|
}
|
|
1775
|
-
const ip = extractIp(req);
|
|
1776
1843
|
const userAgent = req.headers?.["user-agent"] || "";
|
|
1844
|
+
if (isBot(userAgent)) {
|
|
1845
|
+
sendJson(res, 200, { ok: true });
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
const ip = extractIp(req);
|
|
1777
1849
|
const enriched = enrichEvents(payload.events, ip, userAgent);
|
|
1850
|
+
const siteId = enriched[0]?.siteId;
|
|
1851
|
+
if (siteId) {
|
|
1852
|
+
const site = await db.getSite(siteId);
|
|
1853
|
+
if (site?.allowedOrigins && site.allowedOrigins.length > 0) {
|
|
1854
|
+
const allowed = new Set(site.allowedOrigins.map((h) => h.toLowerCase()));
|
|
1855
|
+
const filtered = enriched.filter((event) => {
|
|
1856
|
+
if (!event.url) return true;
|
|
1857
|
+
try {
|
|
1858
|
+
const hostname = new URL(event.url).hostname.toLowerCase();
|
|
1859
|
+
return allowed.has(hostname);
|
|
1860
|
+
} catch {
|
|
1861
|
+
return true;
|
|
1862
|
+
}
|
|
1863
|
+
});
|
|
1864
|
+
if (filtered.length === 0) {
|
|
1865
|
+
sendJson(res, 200, { ok: true });
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
await db.insertEvents(filtered);
|
|
1869
|
+
sendJson(res, 200, { ok: true });
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1778
1873
|
await db.insertEvents(enriched);
|
|
1779
1874
|
sendJson(res, 200, { ok: true });
|
|
1780
1875
|
} catch (err) {
|
|
@@ -2115,6 +2210,7 @@ function sendJson(res, status, body) {
|
|
|
2115
2210
|
0 && (module.exports = {
|
|
2116
2211
|
ClickHouseAdapter,
|
|
2117
2212
|
MongoDBAdapter,
|
|
2118
|
-
createCollector
|
|
2213
|
+
createCollector,
|
|
2214
|
+
isBot
|
|
2119
2215
|
});
|
|
2120
2216
|
//# sourceMappingURL=index.cjs.map
|