hearald 0.0.2 → 0.0.4

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 CHANGED
@@ -1,21 +1,11 @@
1
1
  # hearald
2
2
 
3
- A whimsical event bus and evented utility for client and server, with event tracking, error logging, and analytics/statistics helpers.
4
-
5
- ## Status (August 2025)
6
-
7
- - ✅ Event bus/evented logic fully extracted and modularized
8
- - ✅ Server-side event file logic (read/write/close/load) extracted
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
 
@@ -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
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "hearald",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "A single server drop-in file system based metrics and analytics library.",
5
5
  "main": "src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js",
8
+ "./client": "./src/client.js"
9
+ },
6
10
  "type": "module",
7
11
  "scripts": {
8
12
  "build": "rollup --config"
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
+ }
@@ -2,8 +2,5 @@
2
2
  "env": {
3
3
  "browser": true,
4
4
  "es2020": true
5
- },
6
- "globals": {
7
- "INJECTED__DATE": "readonly"
8
5
  }
9
6
  }
@@ -8,7 +8,7 @@ import {
8
8
  fetchChartData,
9
9
  fetchData,
10
10
  } from "./util.js";
11
- import Tile from "./Tile";
11
+ import Tile from "./Tile.js";
12
12
 
13
13
  const DATE = new Date().toISOString().split("T")[0];
14
14
 
@@ -1,7 +1,5 @@
1
1
  import "../css/analytics.css";
2
2
 
3
- console.log("index.js loaded");
4
-
5
3
  import { render, html } from "htm/preact";
6
4
 
7
5
  import App from "./App";
@@ -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
- console.log(
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
- console.log(
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
- console.error("failed to compute analytics", e);
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
- console.log("filename is", __filename);
271
- console.log("dirname is", __dirname);
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
- console.log(chalk.cyan("[API] GET /analytics/data/:last/:date?"));
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 analyticsHtmlRaw = fileContents.toString("utf-8");
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 createHearaldMiddleware({ url = "/e", parseBody } = {}) {
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
- console.error("error writing event to event log", err);
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
- console.warn(`file not found: events/${fileName}.log`);
85
+ log.warn(`file not found: events/${fileName}.log`);
85
86
  } else {
86
87
  // eslint-disable-next-line no-console
87
- console.error(e);
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