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 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
+ ```
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }