easy-analytics 1.0.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 +113 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +174 -0
- package/dist/common.d.ts +46 -0
- package/dist/common.js +34 -0
- package/dist/react.d.ts +12 -0
- package/dist/react.js +35 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.js +164 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Easy Analytics
|
|
2
|
+
|
|
3
|
+
A super minimalist, lite & easy, independent analytics tracker & visualizer for your custom web applications.
|
|
4
|
+
|
|
5
|
+
Easy Analytics consists of three parts that work seamlessly together:
|
|
6
|
+
1. **Client**: A lightweight browser script (~6 KB uncompressed, ~1.7 KB gzipped) tracking basic metrics (screens, duration, referrers, devices) without relying exclusively on tracking cookies.
|
|
7
|
+
2. **Server**: Server-side integration storing the incoming logs effortlessly using `easy-db-node`.
|
|
8
|
+
3. **React**: Helpful hooks and a minimalist unstyled UI component (`GroupTable`) for you to build your own dashboard quickly.
|
|
9
|
+
|
|
10
|
+
## Philosophy
|
|
11
|
+
|
|
12
|
+
- **Self-hosted**: Say goodbye to ad-blockers preventing analytics. The requests go straight to your backend.
|
|
13
|
+
- **Privacy-first**: It groups users into temporary `sessionId` and permanent `localId` via `localStorage`/`sessionStorage` without obnoxious 3rd party tracker tracking.
|
|
14
|
+
- **Minimalist setup**: No bloated dashboard or configuration pages unless you create them.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Client Integration
|
|
19
|
+
|
|
20
|
+
Call `init` as early as possible in your application lifecycle (for example in your main React `index.tsx`, or inside `<script>` tag).
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { init, sendError } from "easy-analytics/client";
|
|
24
|
+
|
|
25
|
+
// Initializes the tracker.
|
|
26
|
+
// 1st arg: path to the tracking endpoint
|
|
27
|
+
// 2nd arg: token (optional)
|
|
28
|
+
init("/api/easy-analytics", "token");
|
|
29
|
+
|
|
30
|
+
// Example of manual error tracking
|
|
31
|
+
try {
|
|
32
|
+
throw new Error("Something broke!");
|
|
33
|
+
} catch (e) {
|
|
34
|
+
sendError(e);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 2. Server Integration
|
|
39
|
+
|
|
40
|
+
You need a runtime (like `express.js`) to capture incoming events. Easy Analytics uses `easy-db-node` under the hood.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import express from "express";
|
|
44
|
+
import { postEasyAnalytics, getEasyAnalytics, getEasyAnalyticsError } from "easy-analytics/server";
|
|
45
|
+
|
|
46
|
+
const app = express();
|
|
47
|
+
app.use(express.json()); // Need to parse JSON body
|
|
48
|
+
|
|
49
|
+
app.post("/api/easy-analytics", async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
|
52
|
+
const userAgent = req.headers['user-agent'] || "";
|
|
53
|
+
|
|
54
|
+
// Pass body, userAgent, and IP. Optionally pass ipCountry if you resolve it
|
|
55
|
+
const responseData = await postEasyAnalytics(req.body, userAgent, ip, "US");
|
|
56
|
+
res.status(200).send(responseData);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
res.status(400).send({ error: e.message });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Endpoint to retrieve data for your React dashboard
|
|
63
|
+
app.get("/api/easy-analytics-data", async (req, res) => {
|
|
64
|
+
// get records by passing month in YYYY-MM format
|
|
65
|
+
const records = await getEasyAnalytics("2026-04");
|
|
66
|
+
res.status(200).send(records);
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This will automatically track page visits, `visibilitychange` for session lengths, and handle basic errors automatically attaching to `window.onerror`.
|
|
71
|
+
|
|
72
|
+
## 3. Visualization & React Hooks
|
|
73
|
+
|
|
74
|
+
When it's time to analyze your data in an admin dashboard, `easy-analytics/react` provides utility hooks to process and group rows easily.
|
|
75
|
+
|
|
76
|
+
The `<GroupTable />` component is 100% dependency-free and uses standard HTML tables.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import React, { useEffect, useState } from "react";
|
|
80
|
+
import { useGroup, GroupTable, getGroupedCount } from "easy-analytics/react";
|
|
81
|
+
|
|
82
|
+
export function Dashboard() {
|
|
83
|
+
const [records, setRecords] = useState([]);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
fetch("/api/easy-analytics-data")
|
|
87
|
+
.then(res => res.json())
|
|
88
|
+
.then(data => setRecords(data));
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
// Grouping examples
|
|
92
|
+
// 1. Group by pathname
|
|
93
|
+
const byURL = useGroup(records, r => new URL(r.url).pathname);
|
|
94
|
+
|
|
95
|
+
// 2. Group by Referrer but count unique users (localId) instead of pure views
|
|
96
|
+
const byReferrer = useGroup(
|
|
97
|
+
records,
|
|
98
|
+
r => r.referrer || "Direct",
|
|
99
|
+
rs => getGroupedCount(rs, row => row.localId)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div>
|
|
104
|
+
<h1>Analytics Dashboard</h1>
|
|
105
|
+
|
|
106
|
+
<div style={{ display: "flex", gap: "20px" }}>
|
|
107
|
+
<GroupTable title="Most Visited Pages" data={byURL} mainColor="#4287f5" />
|
|
108
|
+
<GroupTable title="Traffic Sources (Unique)" data={byReferrer} mainColor="#7dd421" />
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
```
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { UUID } from "./common";
|
|
2
|
+
export * from "./common";
|
|
3
|
+
export declare function init(url?: string, token?: string): void;
|
|
4
|
+
export declare function getLocalId(): UUID;
|
|
5
|
+
export declare function getSessionId(): UUID;
|
|
6
|
+
export declare function sendError(error: any, sendUrl?: string, token?: string): void;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.init = init;
|
|
18
|
+
exports.getLocalId = getLocalId;
|
|
19
|
+
exports.getSessionId = getSessionId;
|
|
20
|
+
exports.sendError = sendError;
|
|
21
|
+
const common_1 = require("./common");
|
|
22
|
+
__exportStar(require("./common"), exports);
|
|
23
|
+
const STORAGE_ID = "easyAnalyticsId";
|
|
24
|
+
let easyAnalyticsUrl = "/api/easy-analytics";
|
|
25
|
+
let easyAnalyticsToken = "";
|
|
26
|
+
function init(url = easyAnalyticsUrl, token = easyAnalyticsToken) {
|
|
27
|
+
// not run on server site
|
|
28
|
+
if (typeof window === "undefined" || typeof document === "undefined")
|
|
29
|
+
return;
|
|
30
|
+
easyAnalyticsUrl = url;
|
|
31
|
+
easyAnalyticsToken = token;
|
|
32
|
+
let interval = setInterval(() => handleChangeUrl(url, token), 500);
|
|
33
|
+
document.addEventListener("visibilitychange", () => {
|
|
34
|
+
if (document.visibilityState === "visible") {
|
|
35
|
+
if (interval === null)
|
|
36
|
+
interval = setInterval(() => handleChangeUrl(url, token), 500);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// leaving page or tab - can return
|
|
40
|
+
if (interval) {
|
|
41
|
+
handleLeave(url, token);
|
|
42
|
+
clearInterval(interval);
|
|
43
|
+
interval = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
window.addEventListener("beforeunload", () => {
|
|
48
|
+
if (interval)
|
|
49
|
+
handleLeave(url, token);
|
|
50
|
+
});
|
|
51
|
+
// error handle
|
|
52
|
+
// window.onerror = (message, source, lineno, colno, error) => {
|
|
53
|
+
// handleError(url, token, error || message);
|
|
54
|
+
// };
|
|
55
|
+
window.addEventListener("error", (event) => sendError(event.error, url, token));
|
|
56
|
+
window.addEventListener("unhandledrejection", (event) => sendError(event.reason, url, token));
|
|
57
|
+
}
|
|
58
|
+
let localId = null;
|
|
59
|
+
function getLocalId() {
|
|
60
|
+
// not run on server site
|
|
61
|
+
if (typeof localStorage === "undefined")
|
|
62
|
+
return "server";
|
|
63
|
+
if (localId)
|
|
64
|
+
return localId;
|
|
65
|
+
localId = localStorage.getItem(STORAGE_ID);
|
|
66
|
+
if ((0, common_1.isUuidV4)(localId))
|
|
67
|
+
return localId;
|
|
68
|
+
localId = (0, common_1.generateUuidV4)();
|
|
69
|
+
localStorage.setItem(STORAGE_ID, localId);
|
|
70
|
+
return localId;
|
|
71
|
+
}
|
|
72
|
+
let sessionId = null;
|
|
73
|
+
function getSessionId() {
|
|
74
|
+
// not run on server site
|
|
75
|
+
if (typeof sessionStorage === "undefined")
|
|
76
|
+
return "server";
|
|
77
|
+
if (sessionId)
|
|
78
|
+
return sessionId;
|
|
79
|
+
sessionId = sessionStorage.getItem(STORAGE_ID);
|
|
80
|
+
if ((0, common_1.isUuidV4)(sessionId))
|
|
81
|
+
return sessionId;
|
|
82
|
+
sessionId = (0, common_1.generateUuidV4)();
|
|
83
|
+
sessionStorage.setItem(STORAGE_ID, sessionId);
|
|
84
|
+
return sessionId;
|
|
85
|
+
}
|
|
86
|
+
let recordIdBefore = "";
|
|
87
|
+
let urlBefore = "";
|
|
88
|
+
function handleChangeUrl(sendUrl, token) {
|
|
89
|
+
const url = window.location.href;
|
|
90
|
+
if (url === urlBefore)
|
|
91
|
+
return;
|
|
92
|
+
const record = {
|
|
93
|
+
recordIdBefore,
|
|
94
|
+
localId: getLocalId(),
|
|
95
|
+
sessionId: getSessionId(),
|
|
96
|
+
localDate: (0, common_1.getDate)(),
|
|
97
|
+
url,
|
|
98
|
+
urlBefore,
|
|
99
|
+
referrer: document.referrer,
|
|
100
|
+
window: {
|
|
101
|
+
innerWidth: window.innerWidth,
|
|
102
|
+
innerHeight: window.innerHeight,
|
|
103
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
104
|
+
screenWidth: window.screen.width,
|
|
105
|
+
screenHeight: window.screen.height,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
urlBefore = url;
|
|
109
|
+
fetch(sendUrl + (token ? `?token=${encodeURIComponent(token)}` : ""), {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: { "Content-Type": "application/json" },
|
|
112
|
+
body: JSON.stringify(record),
|
|
113
|
+
})
|
|
114
|
+
.then(res => res.json())
|
|
115
|
+
.then(data => {
|
|
116
|
+
if (data && typeof data === "object" && typeof data.id === "string" && data.id) {
|
|
117
|
+
recordIdBefore = data.id;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.warn("Easy Analytics problem with receiving data to server");
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
.catch(() => {
|
|
124
|
+
console.warn("Easy Analytics problem with receiving data to server");
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function handleLeave(sendUrl, token) {
|
|
128
|
+
const record = {
|
|
129
|
+
recordIdBefore,
|
|
130
|
+
localId: getLocalId(),
|
|
131
|
+
sessionId: getSessionId(),
|
|
132
|
+
localDate: (0, common_1.getDate)(),
|
|
133
|
+
};
|
|
134
|
+
const send = navigator.sendBeacon(sendUrl + (token ? `?token=${encodeURIComponent(token)}` : ""), new Blob([JSON.stringify(record)], { type: "application/json" }));
|
|
135
|
+
if (!send) {
|
|
136
|
+
console.warn("Easy Analytics problem with sending data to server");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function sendError(error, sendUrl = easyAnalyticsUrl, token = easyAnalyticsToken) {
|
|
140
|
+
let name;
|
|
141
|
+
let message = "";
|
|
142
|
+
let stack;
|
|
143
|
+
if (error instanceof Error) {
|
|
144
|
+
name = error.name;
|
|
145
|
+
message = error.message;
|
|
146
|
+
stack = error.stack;
|
|
147
|
+
}
|
|
148
|
+
else if (typeof error === "string") {
|
|
149
|
+
message = error;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
message = JSON.stringify(error);
|
|
153
|
+
}
|
|
154
|
+
const record = {
|
|
155
|
+
localId: getLocalId(),
|
|
156
|
+
sessionId: getSessionId(),
|
|
157
|
+
url: window.location.href,
|
|
158
|
+
localDate: (0, common_1.getDate)(),
|
|
159
|
+
error: { name, message, stack },
|
|
160
|
+
};
|
|
161
|
+
fetch(sendUrl + (token ? `?token=${encodeURIComponent(token)}` : ""), {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: { "Content-Type": "application/json" },
|
|
164
|
+
body: JSON.stringify(record),
|
|
165
|
+
})
|
|
166
|
+
.then(res => res.json())
|
|
167
|
+
.then(data => {
|
|
168
|
+
if (!data || typeof data !== "object" || typeof data.id !== "string" && !data.id)
|
|
169
|
+
console.warn("Easy Analytics problem with receiving data to server");
|
|
170
|
+
})
|
|
171
|
+
.catch(() => {
|
|
172
|
+
console.warn("Easy Analytics problem with receiving data to server");
|
|
173
|
+
});
|
|
174
|
+
}
|
package/dist/common.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type UUID = string;
|
|
2
|
+
export type AnalysisRecord = {
|
|
3
|
+
recordIdBefore: string;
|
|
4
|
+
localId: UUID;
|
|
5
|
+
sessionId: UUID;
|
|
6
|
+
serverDate: string;
|
|
7
|
+
localDate: string;
|
|
8
|
+
localDateLeft?: string;
|
|
9
|
+
ip: string;
|
|
10
|
+
ipCountry?: string;
|
|
11
|
+
userAgent: string;
|
|
12
|
+
url: string;
|
|
13
|
+
urlBefore: string;
|
|
14
|
+
referrer: string;
|
|
15
|
+
window: {
|
|
16
|
+
innerWidth: number;
|
|
17
|
+
innerHeight: number;
|
|
18
|
+
devicePixelRatio: number;
|
|
19
|
+
screenWidth: number;
|
|
20
|
+
screenHeight: number;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export type AnalysisRecordBrowser = Omit<AnalysisRecord, "ip" | "userAgent" | "serverDate" | "localDateLeft">;
|
|
24
|
+
export type AnalysisLeaveRecordBrowser = {
|
|
25
|
+
recordIdBefore: string;
|
|
26
|
+
localId: UUID;
|
|
27
|
+
sessionId: UUID;
|
|
28
|
+
localDate: string;
|
|
29
|
+
};
|
|
30
|
+
export type AnalysisErrorRecord = AnalysisErrorRecordBrowser & {
|
|
31
|
+
serverDate: string;
|
|
32
|
+
};
|
|
33
|
+
export type AnalysisErrorRecordBrowser = {
|
|
34
|
+
localId: UUID;
|
|
35
|
+
sessionId: UUID;
|
|
36
|
+
url: string;
|
|
37
|
+
localDate: string;
|
|
38
|
+
error: {
|
|
39
|
+
name?: string;
|
|
40
|
+
message: string;
|
|
41
|
+
stack?: string;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export declare function getDate(date?: Date): string;
|
|
45
|
+
export declare function isUuidV4(id: unknown): id is UUID;
|
|
46
|
+
export declare function generateUuidV4(): UUID;
|
package/dist/common.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getDate = getDate;
|
|
4
|
+
exports.isUuidV4 = isUuidV4;
|
|
5
|
+
exports.generateUuidV4 = generateUuidV4;
|
|
6
|
+
function getDate(date = new Date()) {
|
|
7
|
+
const offset = date.getTimezoneOffset();
|
|
8
|
+
const offsetHours = Math.abs(Math.floor(offset / 60));
|
|
9
|
+
const offsetMinutes = Math.abs(offset % 60);
|
|
10
|
+
const offsetString = (offset > 0 ? '-' : '+') + offsetHours.toFixed(0).padStart(2, '0') + ':' + offsetMinutes.toFixed(0).padStart(2, '0');
|
|
11
|
+
return date.toISOString().replace('Z', offsetString);
|
|
12
|
+
}
|
|
13
|
+
function isUuidV4(id) {
|
|
14
|
+
if (id
|
|
15
|
+
&& typeof id === "string"
|
|
16
|
+
&& id.length === 36
|
|
17
|
+
&& id.charAt(8) === "-"
|
|
18
|
+
&& id.charAt(13) === "-"
|
|
19
|
+
&& id.charAt(18) === "-"
|
|
20
|
+
&& id.charAt(23) === "-"
|
|
21
|
+
&& id.charAt(14) === "4")
|
|
22
|
+
return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
function generateUuidV4() {
|
|
26
|
+
if (window.crypto && window.crypto.randomUUID) {
|
|
27
|
+
return window.crypto.randomUUID();
|
|
28
|
+
}
|
|
29
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
30
|
+
const r = Math.random() * 16 | 0;
|
|
31
|
+
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
32
|
+
return v.toString(16);
|
|
33
|
+
});
|
|
34
|
+
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type GroupLog<T> = {
|
|
2
|
+
key: string;
|
|
3
|
+
records: T[];
|
|
4
|
+
count: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function useGroup<T>(log: T[], cb: (l: T) => string, cbCount?: (l: T[]) => number): GroupLog<T>[];
|
|
7
|
+
export declare function getGroupedCount<T>(records: T[], cb: (r: T) => string): number;
|
|
8
|
+
export declare function GroupTable<T>({ title, data, mainColor }: {
|
|
9
|
+
title: string;
|
|
10
|
+
data: GroupLog<T>[];
|
|
11
|
+
mainColor?: string;
|
|
12
|
+
}): import("react/jsx-runtime").JSX.Element;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useGroup = useGroup;
|
|
4
|
+
exports.getGroupedCount = getGroupedCount;
|
|
5
|
+
exports.GroupTable = GroupTable;
|
|
6
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
function useGroup(log, cb, cbCount = (l) => l.length) {
|
|
9
|
+
return (0, react_1.useMemo)(() => {
|
|
10
|
+
// Fallback for environments where Object.groupBy might not be available yet
|
|
11
|
+
const group = Object.groupBy ? Object.groupBy(log, cb) : fallbackGroupBy(log, cb);
|
|
12
|
+
return Object.entries(group)
|
|
13
|
+
.map(([key, records]) => ({ key, records: records || [], count: cbCount(records || []) }))
|
|
14
|
+
.sort((a, b) => b.count - a.count);
|
|
15
|
+
}, [log]);
|
|
16
|
+
}
|
|
17
|
+
function fallbackGroupBy(log, cb) {
|
|
18
|
+
const group = {};
|
|
19
|
+
for (const item of log) {
|
|
20
|
+
const key = cb(item);
|
|
21
|
+
if (!group[key])
|
|
22
|
+
group[key] = [];
|
|
23
|
+
group[key].push(item);
|
|
24
|
+
}
|
|
25
|
+
return group;
|
|
26
|
+
}
|
|
27
|
+
function getGroupedCount(records, cb) {
|
|
28
|
+
const group = Object.groupBy ? Object.groupBy(records, cb) : fallbackGroupBy(records, cb);
|
|
29
|
+
return Object.keys(group).length;
|
|
30
|
+
}
|
|
31
|
+
function GroupTable({ title, data, mainColor = "#7dd421" }) {
|
|
32
|
+
const max = Math.max(0, ...data.map(d => d.count));
|
|
33
|
+
const sum = data.reduce((a, b) => a + b.count, 0);
|
|
34
|
+
return (0, jsx_runtime_1.jsxs)("div", { style: { width: "100%", maxWidth: "600px", marginBottom: "32px", fontFamily: "sans-serif" }, children: [(0, jsx_runtime_1.jsx)("h2", { style: { paddingTop: "20px", marginBottom: "8px" }, children: title }), (0, jsx_runtime_1.jsxs)("p", { style: { margin: "0 0 16px 0", fontSize: "14px", color: "#666" }, children: ["Z\u00E1znamy: ", Object.keys(data).length] }), (0, jsx_runtime_1.jsxs)("table", { style: { width: "100%", borderCollapse: "collapse" }, children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { style: { textAlign: "left", paddingBottom: "8px", width: "150px" }, children: "Po\u010Det" }), (0, jsx_runtime_1.jsx)("th", { style: { textAlign: "left", paddingBottom: "8px" }, children: "Polo\u017Eka" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: data.map(r => (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsxs)("td", { style: { position: "relative", padding: "4px 0", minWidth: "150px" }, children: [(0, jsx_runtime_1.jsx)("div", { style: { position: "absolute", zIndex: 1, width: `${max > 0 ? (r.count / max * 100) : 0}%`, left: "0px", top: "4px", bottom: "4px", backgroundColor: mainColor, borderRadius: "4px", opacity: 0.8 } }), (0, jsx_runtime_1.jsxs)("div", { style: { position: "relative", zIndex: 2, paddingLeft: "8px", fontWeight: "bold" }, children: [r.count, " ", (0, jsx_runtime_1.jsxs)("span", { style: { fontWeight: "normal", fontSize: "0.85em", opacity: 0.8 }, children: ["(", sum > 0 ? Math.round(r.count / sum * 100) : 0, "%)"] })] })] }), (0, jsx_runtime_1.jsx)("td", { style: { padding: "4px 8px" }, children: (0, jsx_runtime_1.jsx)("div", { style: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: "400px" }, title: r.key, children: r.key }) })] }, r.key)) })] })] });
|
|
35
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AnalysisErrorRecordBrowser, AnalysisRecord } from "./common";
|
|
2
|
+
type Month = string;
|
|
3
|
+
export * from "./common";
|
|
4
|
+
export declare function postEasyAnalytics(body: unknown, userAgent: string, ip: string, ipCountry?: string): Promise<{
|
|
5
|
+
id: string;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function getEasyAnalytics(month: Month): Promise<(AnalysisRecord & {
|
|
8
|
+
_id: import("easy-db-core").Id;
|
|
9
|
+
})[]>;
|
|
10
|
+
export declare function getEasyAnalyticsError(month: Month): Promise<(AnalysisErrorRecordBrowser & {
|
|
11
|
+
serverDate: string;
|
|
12
|
+
} & {
|
|
13
|
+
_id: import("easy-db-core").Id;
|
|
14
|
+
})[]>;
|
|
15
|
+
export declare function updateEasyAnalyticsRecord(month: Month, recordId: string, record: AnalysisRecord): Promise<void>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.postEasyAnalytics = postEasyAnalytics;
|
|
21
|
+
exports.getEasyAnalytics = getEasyAnalytics;
|
|
22
|
+
exports.getEasyAnalyticsError = getEasyAnalyticsError;
|
|
23
|
+
exports.updateEasyAnalyticsRecord = updateEasyAnalyticsRecord;
|
|
24
|
+
const easy_db_node_1 = __importDefault(require("easy-db-node"));
|
|
25
|
+
const common_1 = require("./common");
|
|
26
|
+
const { insert, select, selectArray, update, remove } = (0, easy_db_node_1.default)({});
|
|
27
|
+
__exportStar(require("./common"), exports);
|
|
28
|
+
async function postEasyAnalytics(body, userAgent, ip, ipCountry) {
|
|
29
|
+
const recordBrowser = getAnalysisRecordBrowser(body);
|
|
30
|
+
if (recordBrowser) {
|
|
31
|
+
const serverDate = (0, common_1.getDate)();
|
|
32
|
+
if (recordBrowser.recordIdBefore) {
|
|
33
|
+
await updateLeaveRecord(recordBrowser.recordIdBefore, recordBrowser.localDate);
|
|
34
|
+
}
|
|
35
|
+
const id = await insert(getCollection(serverDate), {
|
|
36
|
+
...recordBrowser,
|
|
37
|
+
ip,
|
|
38
|
+
ipCountry,
|
|
39
|
+
userAgent,
|
|
40
|
+
serverDate,
|
|
41
|
+
});
|
|
42
|
+
return { id };
|
|
43
|
+
}
|
|
44
|
+
const leaveRecordBrowser = getAnalysisLeaveRecordBrowser(body);
|
|
45
|
+
if (leaveRecordBrowser)
|
|
46
|
+
return await updateLeaveRecord(leaveRecordBrowser.recordIdBefore, leaveRecordBrowser.localDate);
|
|
47
|
+
const errorRecordBrowser = getAnalysisErrorRecordBrowser(body);
|
|
48
|
+
if (errorRecordBrowser)
|
|
49
|
+
return {
|
|
50
|
+
id: await insert(getErrorCollection(errorRecordBrowser.localDate), {
|
|
51
|
+
...errorRecordBrowser,
|
|
52
|
+
serverDate: (0, common_1.getDate)(),
|
|
53
|
+
})
|
|
54
|
+
};
|
|
55
|
+
throw new Error("Easy Analytics data are not valid");
|
|
56
|
+
}
|
|
57
|
+
async function getEasyAnalytics(month) {
|
|
58
|
+
return selectArray(getCollection(month));
|
|
59
|
+
}
|
|
60
|
+
async function getEasyAnalyticsError(month) {
|
|
61
|
+
return selectArray(getErrorCollection(month));
|
|
62
|
+
}
|
|
63
|
+
async function updateEasyAnalyticsRecord(month, recordId, record) {
|
|
64
|
+
await update(getCollection(month), recordId, record);
|
|
65
|
+
}
|
|
66
|
+
async function updateLeaveRecord(recordIdBefore, date) {
|
|
67
|
+
const collection = getCollection(date);
|
|
68
|
+
const record = await select(collection, recordIdBefore);
|
|
69
|
+
// TODO: check month back
|
|
70
|
+
if (!record) {
|
|
71
|
+
console.error(new Error("Easy Analytic leave record not found"));
|
|
72
|
+
return { id: recordIdBefore };
|
|
73
|
+
}
|
|
74
|
+
// TODO: check localId and more
|
|
75
|
+
await update(collection, record._id, {
|
|
76
|
+
...record,
|
|
77
|
+
localDateLeft: date,
|
|
78
|
+
});
|
|
79
|
+
return { id: record._id };
|
|
80
|
+
}
|
|
81
|
+
function getCollection(date) {
|
|
82
|
+
return `easyAnalytics-${getMonth(date)}`;
|
|
83
|
+
}
|
|
84
|
+
function getErrorCollection(date) {
|
|
85
|
+
return `easyAnalyticsError-${getMonth(date)}`;
|
|
86
|
+
}
|
|
87
|
+
function getMonth(date = (0, common_1.getDate)()) {
|
|
88
|
+
return date.slice(0, 7);
|
|
89
|
+
}
|
|
90
|
+
function getAnalysisLeaveRecordBrowser(d) {
|
|
91
|
+
if (d !== null
|
|
92
|
+
&& typeof d === "object"
|
|
93
|
+
&& typeof d.recordIdBefore === "string"
|
|
94
|
+
&& typeof d.localId === "string" && d.localId
|
|
95
|
+
&& typeof d.sessionId === "string" && d.sessionId
|
|
96
|
+
&& typeof d.localDate === "string" && d.localDate
|
|
97
|
+
&& typeof d.url === "undefined")
|
|
98
|
+
return {
|
|
99
|
+
recordIdBefore: d.recordIdBefore,
|
|
100
|
+
localId: d.localId,
|
|
101
|
+
sessionId: d.sessionId,
|
|
102
|
+
localDate: d.localDate,
|
|
103
|
+
};
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function getAnalysisRecordBrowser(d) {
|
|
107
|
+
if (d !== null
|
|
108
|
+
&& typeof d === "object"
|
|
109
|
+
&& typeof d.recordIdBefore === "string"
|
|
110
|
+
&& typeof d.localId === "string" && d.localId
|
|
111
|
+
&& typeof d.sessionId === "string" && d.sessionId
|
|
112
|
+
&& typeof d.localDate === "string" && d.localDate
|
|
113
|
+
&& typeof d.url === "string" && d.url
|
|
114
|
+
&& typeof d.urlBefore === "string"
|
|
115
|
+
&& typeof d.referrer === "string"
|
|
116
|
+
&& typeof d.window === "object" && d.window
|
|
117
|
+
&& typeof d.window.innerWidth === "number"
|
|
118
|
+
&& typeof d.window.innerHeight === "number"
|
|
119
|
+
&& typeof d.window.devicePixelRatio === "number"
|
|
120
|
+
&& typeof d.window.screenWidth === "number"
|
|
121
|
+
&& typeof d.window.screenHeight === "number")
|
|
122
|
+
return {
|
|
123
|
+
recordIdBefore: d.recordIdBefore,
|
|
124
|
+
localId: d.localId,
|
|
125
|
+
sessionId: d.sessionId,
|
|
126
|
+
localDate: d.localDate,
|
|
127
|
+
url: d.url,
|
|
128
|
+
urlBefore: d.urlBefore,
|
|
129
|
+
referrer: d.referrer,
|
|
130
|
+
window: {
|
|
131
|
+
innerWidth: d.window.innerWidth,
|
|
132
|
+
innerHeight: d.window.innerHeight,
|
|
133
|
+
devicePixelRatio: d.window.devicePixelRatio,
|
|
134
|
+
screenWidth: d.window.screenWidth,
|
|
135
|
+
screenHeight: d.window.screenHeight,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function getAnalysisErrorRecordBrowser(d) {
|
|
141
|
+
if (d !== null
|
|
142
|
+
&& typeof d === "object"
|
|
143
|
+
&& typeof d.localId === "string" && d.localId
|
|
144
|
+
&& typeof d.sessionId === "string" && d.sessionId
|
|
145
|
+
&& typeof d.localDate === "string" && d.localDate
|
|
146
|
+
&& typeof d.url === "string"
|
|
147
|
+
&& d.error
|
|
148
|
+
&& typeof d.error === "object"
|
|
149
|
+
&& typeof d.error.message === "string"
|
|
150
|
+
&& (typeof d.error.name === "string" || typeof d.error.name === "undefined")
|
|
151
|
+
&& (typeof d.error.stack === "string" || typeof d.error.stack === "undefined"))
|
|
152
|
+
return {
|
|
153
|
+
localId: d.localId,
|
|
154
|
+
sessionId: d.sessionId,
|
|
155
|
+
url: d.url,
|
|
156
|
+
localDate: d.localDate,
|
|
157
|
+
error: {
|
|
158
|
+
name: d.error.name,
|
|
159
|
+
message: d.error.message,
|
|
160
|
+
stack: d.error.stack,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
return null;
|
|
164
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "easy-analytics",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Minimalist analytics tracker and visualizer with client and server integration.",
|
|
5
|
+
"main": "dist/server.js",
|
|
6
|
+
"types": "dist/common.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/common.d.ts",
|
|
10
|
+
"import": "./dist/client.js",
|
|
11
|
+
"require": "./dist/server.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"types": "./dist/client.d.ts",
|
|
15
|
+
"default": "./dist/client.js"
|
|
16
|
+
},
|
|
17
|
+
"./server": {
|
|
18
|
+
"types": "./dist/server.d.ts",
|
|
19
|
+
"default": "./dist/server.js"
|
|
20
|
+
},
|
|
21
|
+
"./react": {
|
|
22
|
+
"types": "./dist/react.d.ts",
|
|
23
|
+
"default": "./dist/react.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "npx tsc"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"easy-db-node": "^3.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
34
|
+
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"react": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
40
|
+
"react-dom": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/react": "^19.0.0",
|
|
46
|
+
"typescript": "^5.x.x"
|
|
47
|
+
},
|
|
48
|
+
"author": "Filip Paulů <ing.fenix@seznam.cz>",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/ingSlonik/easy-analytics.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/ingSlonik/easy-analytics/issues"
|
|
55
|
+
},
|
|
56
|
+
"license": "MIT"
|
|
57
|
+
}
|