hearald 0.0.2 → 0.0.3
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 +6 -16
- package/dist/analytics.html +0 -3
- package/dist/index.js +0 -3
- package/package.json +1 -1
- package/rollup.config.js +2 -2
- package/src/client.js +105 -0
- package/src/index.js +25 -0
- package/{public → src/public}/js/.eslintrc +0 -3
- package/{public → src/public}/js/App.js +1 -1
- package/{public → src/public}/js/index.js +0 -2
- package/{public → src/public}/js/util.js +0 -1
- package/{server → src/server}/analytics.js +11 -30
- package/src/server/logger.js +33 -0
- package/{server → src/server}/server.js +3 -7
- package/{server → src/server}/serverEvents.js +3 -2
- package/shared/events.js +0 -44
- /package/{public → src/public}/analytics.html +0 -0
- /package/{public → src/public}/css/analytics.css +0 -0
- /package/{public → src/public}/js/Tile.js +0 -0
- /package/{server → src/server}/serverTrackError.js +0 -0
- /package/{shared → src/shared}/utils.js +0 -0
package/README.md
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
# hearald
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- ✅ Client/server event emit logic and `/e` endpoint extracted
|
|
10
|
-
- ✅ Admin stats endpoint and client fetch utility extracted
|
|
11
|
-
- ✅ Analytics client helpers (iso, getData, fetchChartData, fetchData, numFormatter, dataCache) extracted
|
|
12
|
-
- ✅ Client/server/shared subfolders created and migration in progress
|
|
13
|
-
- ✅ Error tracking utilities and Express error middleware added
|
|
14
|
-
- ✅ Integration examples and usage documentation provided
|
|
15
|
-
- ✅ `.eslintrc` added to declare `Intl` as a global for linting
|
|
16
|
-
- ✅ Lint passes and all functionality verified
|
|
17
|
-
- ⏳ Migration of all helpers to client/server/shared nearly complete
|
|
18
|
-
- ⏳ Custom event tracking, configurable stat definitions, embeddable admin UI, and further documentation/testing are next
|
|
3
|
+
A single server drop-in file system based metrics and analytics library.
|
|
4
|
+
|
|
5
|
+
## TODO
|
|
6
|
+
|
|
7
|
+
- top stats histogram should/could be it's own ui-configurable data type
|
|
8
|
+
- top stats histogram fields should be controlled in the ui configuration settings
|
|
19
9
|
|
|
20
10
|
## Usage
|
|
21
11
|
|
package/dist/analytics.html
CHANGED
|
@@ -427,7 +427,6 @@ th {
|
|
|
427
427
|
const endpoint = endpointBuilder
|
|
428
428
|
? endpointBuilder(n, d, q)
|
|
429
429
|
: `/data/${n}/${d}${q ? "?" + q : ""}`;
|
|
430
|
-
console.log("endpoint", endpoint);
|
|
431
430
|
|
|
432
431
|
const data = await fetchStats(endpoint);
|
|
433
432
|
dataCache[key] = data;
|
|
@@ -931,8 +930,6 @@ th {
|
|
|
931
930
|
}
|
|
932
931
|
}
|
|
933
932
|
|
|
934
|
-
console.log("index.js loaded");
|
|
935
|
-
|
|
936
933
|
G(m` <${App} /> `, document.getElementById("analytics-root"));
|
|
937
934
|
|
|
938
935
|
})();
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
const endpoint = endpointBuilder
|
|
30
30
|
? endpointBuilder(n, d, q)
|
|
31
31
|
: `/data/${n}/${d}${q ? "?" + q : ""}`;
|
|
32
|
-
console.log("endpoint", endpoint);
|
|
33
32
|
|
|
34
33
|
const data = await fetchStats(endpoint);
|
|
35
34
|
dataCache[key] = data;
|
|
@@ -533,8 +532,6 @@
|
|
|
533
532
|
}
|
|
534
533
|
}
|
|
535
534
|
|
|
536
|
-
console.log("index.js loaded");
|
|
537
|
-
|
|
538
535
|
G(m` <${App} /> `, document.getElementById("analytics-root"));
|
|
539
536
|
|
|
540
537
|
})();
|
package/package.json
CHANGED
package/rollup.config.js
CHANGED
|
@@ -40,7 +40,7 @@ function inlineAssetsPlugin({ htmlFile, outputFile }) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export default {
|
|
43
|
-
input: "public/js/index.js", // Use JS entry point
|
|
43
|
+
input: "src/public/js/index.js", // Use JS entry point
|
|
44
44
|
output: {
|
|
45
45
|
dir: "dist",
|
|
46
46
|
format: "iife",
|
|
@@ -52,7 +52,7 @@ export default {
|
|
|
52
52
|
extract: true,
|
|
53
53
|
}),
|
|
54
54
|
inlineAssetsPlugin({
|
|
55
|
-
htmlFile: "public/analytics.html",
|
|
55
|
+
htmlFile: "src/public/analytics.html",
|
|
56
56
|
outputFile: "analytics.html",
|
|
57
57
|
}),
|
|
58
58
|
],
|
package/src/client.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { EventBus } from "bussin-js";
|
|
2
|
+
|
|
3
|
+
// Shared event bus for all tracking events
|
|
4
|
+
export const eventBus = new EventBus([]);
|
|
5
|
+
|
|
6
|
+
function trackEvent({ eventName, userId = null, payload = {} } = {}) {
|
|
7
|
+
eventBus.emit("event", { eventName, userId, payload });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const generateVisitorData = () => ({
|
|
11
|
+
user_agent: navigator.userAgent || "unknown",
|
|
12
|
+
referrer: document.referrer || "none",
|
|
13
|
+
url: window.location.toString(),
|
|
14
|
+
screen_resolution: `${screen.width}x${screen.height}`,
|
|
15
|
+
window_resolution: `${window.innerWidth}x${window.innerHeight}`,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const trackError = ({
|
|
19
|
+
error,
|
|
20
|
+
userId = null,
|
|
21
|
+
payload: extraPayload = {},
|
|
22
|
+
} = {}) => {
|
|
23
|
+
const errorEvent = error;
|
|
24
|
+
const errorObject = error.error;
|
|
25
|
+
|
|
26
|
+
let source;
|
|
27
|
+
if (errorObject.stack) {
|
|
28
|
+
const splitStack = errorObject.stack.split("\n");
|
|
29
|
+
if (splitStack.length > 2) {
|
|
30
|
+
source = splitStack[1].trim();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
trackEvent({
|
|
35
|
+
eventName: "error",
|
|
36
|
+
userId,
|
|
37
|
+
payload: {
|
|
38
|
+
message: errorObject.message,
|
|
39
|
+
source,
|
|
40
|
+
file_name: errorEvent.filename || errorObject.fileName,
|
|
41
|
+
line_number: errorEvent.lineno || errorObject.lineNumber,
|
|
42
|
+
column_number: errorEvent.colno || errorObject.columnNumber,
|
|
43
|
+
...generateVisitorData(),
|
|
44
|
+
...extraPayload,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const trackVisit = ({ userId, payload: extraPayload = {} } = {}) => {
|
|
50
|
+
trackEvent({
|
|
51
|
+
eventName: "visit",
|
|
52
|
+
userId,
|
|
53
|
+
payload: {
|
|
54
|
+
...generateVisitorData(),
|
|
55
|
+
...extraPayload,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function emitEventToServer({
|
|
61
|
+
endpoint = "/e",
|
|
62
|
+
eventName,
|
|
63
|
+
userId,
|
|
64
|
+
payload = {},
|
|
65
|
+
fetchImpl = fetch,
|
|
66
|
+
}) {
|
|
67
|
+
return fetchImpl(endpoint, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({ e: eventName, u: userId, p: payload }),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default function hearald({
|
|
77
|
+
endpoint = "/e",
|
|
78
|
+
fetchImpl = fetch,
|
|
79
|
+
getUserId = () => null,
|
|
80
|
+
onError = () => {} /* err, eventName, userId, payload */,
|
|
81
|
+
} = {}) {
|
|
82
|
+
eventBus.on("event", async (event) => {
|
|
83
|
+
const { eventName, userId: emittedUserId, payload } = event;
|
|
84
|
+
const userId = emittedUserId || getUserId() || null;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await emitEventToServer({
|
|
88
|
+
endpoint,
|
|
89
|
+
eventName,
|
|
90
|
+
userId,
|
|
91
|
+
payload,
|
|
92
|
+
fetchImpl,
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
onError(err, eventName, userId, payload);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
eventBus,
|
|
101
|
+
trackEvent,
|
|
102
|
+
trackError,
|
|
103
|
+
trackVisit,
|
|
104
|
+
};
|
|
105
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import log from "./server/logger.js";
|
|
2
|
+
import { eventEndpointMiddleware, shutDown } from "./server/server.js";
|
|
3
|
+
import { createAnalyticsRouter } from "./server/analytics.js";
|
|
4
|
+
|
|
5
|
+
export default function hearald({
|
|
6
|
+
logLevel,
|
|
7
|
+
eventEndpoint: { url = "/e", parseBody } = {},
|
|
8
|
+
analytics: { enabled: analyticsEnabled = true, statDefinitions } = {},
|
|
9
|
+
} = {}) {
|
|
10
|
+
if (logLevel) {
|
|
11
|
+
log.setLogLevel(logLevel);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const eventMiddleware = eventEndpointMiddleware({ url, parseBody });
|
|
15
|
+
|
|
16
|
+
const analyticsMiddleware = analyticsEnabled
|
|
17
|
+
? createAnalyticsRouter({ statDefinitions })
|
|
18
|
+
: null;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
eventMiddleware,
|
|
22
|
+
analyticsMiddleware,
|
|
23
|
+
shutDown,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -14,7 +14,6 @@ export async function getData({ n, d, q, endpointBuilder, fetchStats }) {
|
|
|
14
14
|
const endpoint = endpointBuilder
|
|
15
15
|
? endpointBuilder(n, d, q)
|
|
16
16
|
: `/data/${n}/${d}${q ? "?" + q : ""}`;
|
|
17
|
-
console.log("endpoint", endpoint);
|
|
18
17
|
|
|
19
18
|
const data = await fetchStats(endpoint);
|
|
20
19
|
dataCache[key] = data;
|
|
@@ -4,6 +4,7 @@ import path from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { readFile, writeFile } from "fs/promises";
|
|
6
6
|
import express from "express";
|
|
7
|
+
import log from "./logger.js";
|
|
7
8
|
import {
|
|
8
9
|
omitProperties,
|
|
9
10
|
iso,
|
|
@@ -234,13 +235,13 @@ export async function handleAnalyticsDataRequest({
|
|
|
234
235
|
}
|
|
235
236
|
const dataFromFile = await readMetricsFromCache(endDateIso, timeWindow);
|
|
236
237
|
if (dataFromFile) {
|
|
237
|
-
|
|
238
|
+
log.info(
|
|
238
239
|
`Analytics for ${timeWindow} days ending on ${endDateIso} returned from cache`
|
|
239
240
|
);
|
|
240
241
|
response.status(200).type("application/json").send(dataFromFile);
|
|
241
242
|
return;
|
|
242
243
|
}
|
|
243
|
-
|
|
244
|
+
log.info(
|
|
244
245
|
`Analytics for ${timeWindow} days ending on ${endDateIso} computing from events`
|
|
245
246
|
);
|
|
246
247
|
try {
|
|
@@ -256,7 +257,7 @@ export async function handleAnalyticsDataRequest({
|
|
|
256
257
|
);
|
|
257
258
|
response.status(200).type("application/json").send(JSON.stringify(stats));
|
|
258
259
|
} catch (e) {
|
|
259
|
-
|
|
260
|
+
log.error("failed to compute analytics", e);
|
|
260
261
|
response
|
|
261
262
|
.status(500)
|
|
262
263
|
.type("application/json")
|
|
@@ -267,22 +268,15 @@ export async function handleAnalyticsDataRequest({
|
|
|
267
268
|
const __filename = fileURLToPath(import.meta.url);
|
|
268
269
|
const __dirname = path.dirname(__filename);
|
|
269
270
|
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
log.info("filename is", __filename);
|
|
272
|
+
log.info("dirname is", __dirname);
|
|
272
273
|
|
|
273
|
-
export function createAnalyticsRouter({
|
|
274
|
-
statDefinitions
|
|
275
|
-
loadEventsOverWindow,
|
|
276
|
-
templateReplaceMany,
|
|
277
|
-
analyticsHtmlPath,
|
|
278
|
-
enableLiveReload,
|
|
279
|
-
environment,
|
|
280
|
-
}) {
|
|
281
|
-
console.log("statManifest", statDefinitions);
|
|
274
|
+
export function createAnalyticsRouter({ statDefinitions }) {
|
|
275
|
+
log.info("statManifest", statDefinitions);
|
|
282
276
|
const router = express.Router();
|
|
283
277
|
|
|
284
278
|
router.get(`/data/:last/:date?`, async (request, response) => {
|
|
285
|
-
|
|
279
|
+
log.info(chalk.cyan("[API] GET /analytics/data/:last/:date?"));
|
|
286
280
|
await handleAnalyticsDataRequest({
|
|
287
281
|
request,
|
|
288
282
|
response,
|
|
@@ -297,23 +291,10 @@ export function createAnalyticsRouter({
|
|
|
297
291
|
});
|
|
298
292
|
|
|
299
293
|
router.get(`/`, async (request, response) => {
|
|
300
|
-
console.log(chalk.cyan("[Render] views/analytics.html loading"));
|
|
301
|
-
console.log(path.join(__dirname, "..", "dist", "analytics.html"));
|
|
302
294
|
const fileContents = await readFile(
|
|
303
|
-
path.join(__dirname, "..", "dist", "analytics.html")
|
|
295
|
+
path.join(__dirname, "..", "..", "dist", "analytics.html")
|
|
304
296
|
);
|
|
305
|
-
const
|
|
306
|
-
console.log("analyticsHtmlRaw", analyticsHtmlRaw);
|
|
307
|
-
const yesterday = nowAsPstDate();
|
|
308
|
-
yesterday.setDate(yesterday.getDate() - 1);
|
|
309
|
-
const analyticsHtml = templateReplaceMany(analyticsHtmlRaw, {
|
|
310
|
-
date: yesterday.toISOString().split("T")[0],
|
|
311
|
-
devHeadExtras:
|
|
312
|
-
environment === "development" && enableLiveReload
|
|
313
|
-
? '<script src="/liveReloading.js"></script>'
|
|
314
|
-
: "",
|
|
315
|
-
});
|
|
316
|
-
console.log("analyticsHtml", analyticsHtml);
|
|
297
|
+
const analyticsHtml = fileContents.toString("utf-8");
|
|
317
298
|
response.send(analyticsHtml);
|
|
318
299
|
});
|
|
319
300
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class Logger {
|
|
2
|
+
constructor(level) {
|
|
3
|
+
this.level = level || "info";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
setLogLevel(level) {
|
|
7
|
+
this.level = level;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
debug(...args) {
|
|
11
|
+
if (this.level === "debug") {
|
|
12
|
+
console.log("[DEBUG]", ...args);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
info(...args) {
|
|
17
|
+
if (this.level === "debug" || this.level === "info") {
|
|
18
|
+
console.log("[INFO]", ...args);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
warn(...args) {
|
|
23
|
+
if (this.level !== "error") {
|
|
24
|
+
console.warn("[WARN]", ...args);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
error(...args) {
|
|
29
|
+
console.error("[ERROR]", ...args);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default new Logger("error");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Express middleware for hearald event tracking
|
|
2
2
|
import express from "express";
|
|
3
|
+
import log from "./logger.js";
|
|
3
4
|
import { nowAsPstDate } from "../shared/utils.js";
|
|
4
5
|
import { writeEvent, closeEventFiles } from "./serverEvents.js";
|
|
5
6
|
import { createAnalyticsRouter } from "./analytics.js";
|
|
@@ -19,7 +20,7 @@ export async function shutDown() {
|
|
|
19
20
|
* @param {function} [options.parseBody] - Custom request body parser (req => {e, u, p}).
|
|
20
21
|
* @returns {function} Express middleware
|
|
21
22
|
*/
|
|
22
|
-
export function
|
|
23
|
+
export function eventEndpointMiddleware({ url = "/e", parseBody } = {}) {
|
|
23
24
|
const router = express.Router();
|
|
24
25
|
router.post(url, async (req, res) => {
|
|
25
26
|
try {
|
|
@@ -35,14 +36,9 @@ export function createHearaldMiddleware({ url = "/e", parseBody } = {}) {
|
|
|
35
36
|
await logEvent(nowAsPstDate(), { e, u, p });
|
|
36
37
|
} catch (err) {
|
|
37
38
|
// eslint-disable-next-line no-console
|
|
38
|
-
|
|
39
|
+
log.error("error writing event to event log", err);
|
|
39
40
|
}
|
|
40
41
|
res.status(204).send();
|
|
41
42
|
});
|
|
42
43
|
return router;
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
-
export function createHearaldAnalyticsRouter(options = {}) {
|
|
46
|
-
// Pass statManifest to the analytics router so /analytics/layout.json is available
|
|
47
|
-
return createAnalyticsRouter({ ...options });
|
|
48
|
-
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, open as openFile, mkdir } from "fs/promises";
|
|
2
2
|
|
|
3
|
+
import log from "./logger.js";
|
|
3
4
|
import { iso } from "../shared/utils.js";
|
|
4
5
|
|
|
5
6
|
const eventFilePromises = new Map();
|
|
@@ -81,10 +82,10 @@ export const readEventsFromFile = async (fileName) => {
|
|
|
81
82
|
} catch (e) {
|
|
82
83
|
if (e.message && e.message.indexOf("ENOENT") !== -1) {
|
|
83
84
|
// eslint-disable-next-line no-console
|
|
84
|
-
|
|
85
|
+
log.warn(`file not found: events/${fileName}.log`);
|
|
85
86
|
} else {
|
|
86
87
|
// eslint-disable-next-line no-console
|
|
87
|
-
|
|
88
|
+
log.error(e);
|
|
88
89
|
}
|
|
89
90
|
return [];
|
|
90
91
|
}
|
package/shared/events.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// Usage:
|
|
2
|
-
// 1. Register custom events with a schema
|
|
3
|
-
// 2. Emit custom events by name
|
|
4
|
-
|
|
5
|
-
import { EventBus } from "bussin-js";
|
|
6
|
-
|
|
7
|
-
// Shared event bus for all tracking events
|
|
8
|
-
export const eventBus = new EventBus([]);
|
|
9
|
-
|
|
10
|
-
const registeredEvents = {};
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Registers a custom event type and its schema.
|
|
14
|
-
* @param {string} eventName - The name of the event.
|
|
15
|
-
* @param {object} schema - The schema definition for the event payload.
|
|
16
|
-
*/
|
|
17
|
-
export function registerEvent(eventName, schema) {
|
|
18
|
-
// Prevent duplicate registration
|
|
19
|
-
if (registeredEvents[eventName]) return;
|
|
20
|
-
registeredEvents[eventName] = schema;
|
|
21
|
-
// Add the event type to the tracking event bus if not already present
|
|
22
|
-
if (!eventBus.hasType(eventName)) {
|
|
23
|
-
eventBus.addType(eventName);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Emits a custom event if registered and payload matches schema.
|
|
29
|
-
* @param {string} eventName - The name of the event to emit.
|
|
30
|
-
* @param {object} payload - The event payload.
|
|
31
|
-
* @returns {boolean} - True if emitted, false if not registered or invalid payload.
|
|
32
|
-
*/
|
|
33
|
-
export function emitEvent(eventName, payload) {
|
|
34
|
-
const schema = registeredEvents[eventName];
|
|
35
|
-
if (!schema) return false;
|
|
36
|
-
// Simple schema validation: check required keys and types
|
|
37
|
-
for (const key in schema) {
|
|
38
|
-
if (!(key in payload)) return false;
|
|
39
|
-
if (typeof payload[key] !== schema[key]) return false;
|
|
40
|
-
}
|
|
41
|
-
// Emit on the shared tracking event bus
|
|
42
|
-
eventBus.emit(eventName, payload);
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|