hearald 0.0.3 → 0.0.5

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.
@@ -0,0 +1,9 @@
1
+ .env
2
+ .data
3
+ .DS_Store
4
+ node_modules
5
+ .lesshst
6
+ .sqlite_history
7
+ *.md
8
+ .github
9
+ dist/
package/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.0.5] - 2025-10-05
11
+
12
+ ### Added
13
+
14
+ - Export `errorMiddlware`, `trackServerEvent`, `trackServerError` as part of default `hearald` in `src/index.js'
15
+
16
+ ### Fixed
17
+
18
+ - Bugfix to restore intra-day timestamp granularity of events.
19
+ - Percent formatted numbers now use proper formatter
20
+
21
+ ### Changed
22
+
23
+ - All server-side configuration parameters are defined in `src/server/configuration.js`
24
+
25
+ ## [0.0.4] - 2025-09-21
26
+
27
+ ### Changed
28
+
29
+ - Use package.json `exports`
30
+
31
+ ## [0.0.3] - 2025-09-19
32
+
33
+ ### Added
34
+
35
+ - Initial release of Hearald
36
+
37
+ [0.0.5]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.4...v0.0.5
38
+ [0.0.4]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.3...v0.0.4
39
+ [0.0.3]: https://github.com/olivierlacan/keep-a-changelog/releases/tag/v0.0.3
40
+
41
+ <!--
42
+ `Added` for new features.
43
+ `Changed` for changes in existing functionality.
44
+ `Deprecated` for soon-to-be removed features.
45
+ `Removed` for now removed features.
46
+ `Fixed` for any bug fixes.
47
+ `Security` in case of vulnerabilities.
48
+ -->
@@ -134,7 +134,7 @@ th {
134
134
  @media screen and (max-width: 640px) {
135
135
  body {
136
136
  font-size: 12px;
137
- font-feature-settings: 'lnum';
137
+ font-feature-settings: "lnum";
138
138
  }
139
139
  }
140
140
 
@@ -264,7 +264,7 @@ th {
264
264
  word-break: keep-all;
265
265
  }
266
266
  .bar-chart__bar__label:after {
267
- content: '';
267
+ content: "";
268
268
  width: 12px;
269
269
  height: 12px;
270
270
  background: var(--color-label);
@@ -915,8 +915,8 @@ th {
915
915
  type="${f.type}"
916
916
  value=${deepGet(stats.metrics, f.key)}
917
917
  span=${f.span || null}
918
- ${f.type === "percent" ? "percent" : ""}
919
- ${f.type === "histogram" ? "histogram" : ""}
918
+ percent=${f.type === "percent"}
919
+ histogram=${f.type === "histogram"}
920
920
  />
921
921
  `;
922
922
  })}
package/dist/index.css CHANGED
@@ -122,7 +122,7 @@ th {
122
122
  @media screen and (max-width: 640px) {
123
123
  body {
124
124
  font-size: 12px;
125
- font-feature-settings: 'lnum';
125
+ font-feature-settings: "lnum";
126
126
  }
127
127
  }
128
128
 
@@ -252,7 +252,7 @@ th {
252
252
  word-break: keep-all;
253
253
  }
254
254
  .bar-chart__bar__label:after {
255
- content: '';
255
+ content: "";
256
256
  width: 12px;
257
257
  height: 12px;
258
258
  background: var(--color-label);
package/dist/index.js CHANGED
@@ -517,8 +517,8 @@
517
517
  type="${f.type}"
518
518
  value=${deepGet(stats.metrics, f.key)}
519
519
  span=${f.span || null}
520
- ${f.type === "percent" ? "percent" : ""}
521
- ${f.type === "histogram" ? "histogram" : ""}
520
+ percent=${f.type === "percent"}
521
+ histogram=${f.type === "histogram"}
522
522
  />
523
523
  `;
524
524
  })}
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "hearald",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
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/src/index.js CHANGED
@@ -1,14 +1,31 @@
1
- import log from "./server/logger.js";
2
- import { eventEndpointMiddleware, shutDown } from "./server/server.js";
1
+ import hearaldConfiguration from "./server/configuration.js";
2
+
3
+ import {
4
+ eventEndpointMiddleware,
5
+ trackServerEvent,
6
+ trackServerError,
7
+ errorTrackingMiddleware,
8
+ shutDown,
9
+ } from "./server/server.js";
3
10
  import { createAnalyticsRouter } from "./server/analytics.js";
4
11
 
5
12
  export default function hearald({
13
+ /* singleton configuration options */
6
14
  logLevel,
15
+ nowFn /* () => nowAsDateString */,
16
+ getUserId /* (req) => userId */,
17
+
7
18
  eventEndpoint: { url = "/e", parseBody } = {},
8
19
  analytics: { enabled: analyticsEnabled = true, statDefinitions } = {},
9
20
  } = {}) {
10
21
  if (logLevel) {
11
- log.setLogLevel(logLevel);
22
+ hearaldConfiguration.setLogLevel(logLevel);
23
+ }
24
+ if (nowFn) {
25
+ hearaldConfiguration.setNowFn(nowFn);
26
+ }
27
+ if (getUserId) {
28
+ hearaldConfiguration.setGetUserId(getUserId);
12
29
  }
13
30
 
14
31
  const eventMiddleware = eventEndpointMiddleware({ url, parseBody });
@@ -17,9 +34,14 @@ export default function hearald({
17
34
  ? createAnalyticsRouter({ statDefinitions })
18
35
  : null;
19
36
 
37
+ const errorMiddlware = errorTrackingMiddleware({});
38
+
20
39
  return {
21
- eventMiddleware,
22
- analyticsMiddleware,
40
+ errorMiddlware /* app.use(errorMiddlware) */,
41
+ eventMiddleware /* app.use(eventMiddleware) */,
42
+ analyticsMiddleware /* app.use('/analytics', analyticsMiddleware) */,
43
+ trackServerEvent /* ({ event, time = null, userId, params = {} }) => Promise } */,
44
+ trackServerError /* ({ error, userId = null, extraParams = {} }) => Promise } */,
23
45
  shutDown,
24
46
  };
25
47
  }
@@ -122,7 +122,7 @@ th {
122
122
  @media screen and (max-width: 640px) {
123
123
  body {
124
124
  font-size: 12px;
125
- font-feature-settings: 'lnum';
125
+ font-feature-settings: "lnum";
126
126
  }
127
127
  }
128
128
 
@@ -252,7 +252,7 @@ th {
252
252
  word-break: keep-all;
253
253
  }
254
254
  .bar-chart__bar__label:after {
255
- content: '';
255
+ content: "";
256
256
  width: 12px;
257
257
  height: 12px;
258
258
  background: var(--color-label);
@@ -303,8 +303,8 @@ class App extends Component {
303
303
  type="${f.type}"
304
304
  value=${deepGet(stats.metrics, f.key)}
305
305
  span=${f.span || null}
306
- ${f.type === "percent" ? "percent" : ""}
307
- ${f.type === "histogram" ? "histogram" : ""}
306
+ percent=${f.type === "percent"}
307
+ histogram=${f.type === "histogram"}
308
308
  />
309
309
  `;
310
310
  })}
@@ -5,12 +5,8 @@ import { fileURLToPath } from "url";
5
5
  import { readFile, writeFile } from "fs/promises";
6
6
  import express from "express";
7
7
  import log from "./logger.js";
8
- import {
9
- omitProperties,
10
- iso,
11
- defaultEndDate,
12
- nowAsPstDate,
13
- } from "../shared/utils.js";
8
+ import hearaldConfiguration from "./configuration.js";
9
+ import { omitProperties, defaultEndDate } from "../shared/utils.js";
14
10
  import { loadEventsOverWindow } from "./serverEvents.js";
15
11
 
16
12
  export function generateRawMetrics(events, rawMetricDefinitions) {
@@ -106,7 +102,8 @@ export function generateComputedMetric(metrics, definition, timeWindow) {
106
102
  }
107
103
 
108
104
  export function cacheFilePath(endDate, timeWindow) {
109
- const endDateIso = typeof endDate === "string" ? endDate : iso(endDate);
105
+ const endDateIso =
106
+ typeof endDate === "string" ? endDate : hearaldConfiguration.isoFn(endDate);
110
107
  return `events/summary:${endDateIso}:${timeWindow}.json`;
111
108
  }
112
109
 
@@ -213,7 +210,7 @@ export async function generateStats(statDefinitions, endDate, timeWindow) {
213
210
  });
214
211
  return {
215
212
  metrics: nestedMetrics,
216
- endDate: iso(endDate),
213
+ endDate: hearaldConfiguration.isoFn(endDate),
217
214
  timeWindow,
218
215
  processedEventCount: allEvents.length,
219
216
  };
@@ -224,7 +221,7 @@ export async function handleAnalyticsDataRequest({
224
221
  response,
225
222
  statDefinitions,
226
223
  }) {
227
- const defaultEndDateIso = iso(defaultEndDate(nowAsPstDate));
224
+ const defaultEndDateIso = hearaldConfiguration.isoFn(defaultEndDate());
228
225
  const endDateIso = request.params.date
229
226
  ? request.params.date
230
227
  : defaultEndDateIso;
@@ -282,7 +279,6 @@ export function createAnalyticsRouter({ statDefinitions }) {
282
279
  response,
283
280
  statDefinitions,
284
281
  loadEventsOverWindow,
285
- nowAsPstDate,
286
282
  });
287
283
  });
288
284
 
@@ -0,0 +1,33 @@
1
+ import { iso, nowAsPstDate } from "../shared/utils.js";
2
+
3
+ class Configuration {
4
+ constructor({
5
+ nowFn = nowAsPstDate,
6
+ isoFn = iso,
7
+ logLevel = "info",
8
+ getUserId = (req) => req.user?.id,
9
+ } = {}) {
10
+ this.nowFn = nowFn;
11
+ this.isoFn = isoFn;
12
+ this.logLevel = logLevel;
13
+ this.getUserId = getUserId;
14
+ }
15
+
16
+ setLogLevel(level) {
17
+ this.logLevel = level;
18
+ }
19
+
20
+ setNowFn(nowFn) {
21
+ this.nowFn = nowFn;
22
+ }
23
+
24
+ setGetUserId(getUserId) {
25
+ this.getUserId = getUserId;
26
+ }
27
+
28
+ setIsoFn(isoFn) {
29
+ this.isoFn = isoFn;
30
+ }
31
+ }
32
+
33
+ export default new Configuration();
@@ -1,26 +1,24 @@
1
- class Logger {
2
- constructor(level) {
3
- this.level = level || "info";
4
- }
1
+ import hearaldConfiguration from "./configuration.js";
5
2
 
6
- setLogLevel(level) {
7
- this.level = level;
3
+ class Logger {
4
+ constructor(config = hearaldConfiguration) {
5
+ this.config = config;
8
6
  }
9
7
 
10
8
  debug(...args) {
11
- if (this.level === "debug") {
9
+ if (this.config.logLevel === "debug") {
12
10
  console.log("[DEBUG]", ...args);
13
11
  }
14
12
  }
15
13
 
16
14
  info(...args) {
17
- if (this.level === "debug" || this.level === "info") {
15
+ if (this.config.logLevel === "debug" || this.config.logLevel === "info") {
18
16
  console.log("[INFO]", ...args);
19
17
  }
20
18
  }
21
19
 
22
20
  warn(...args) {
23
- if (this.level !== "error") {
21
+ if (this.config.logLevel !== "error") {
24
22
  console.warn("[WARN]", ...args);
25
23
  }
26
24
  }
@@ -1,13 +1,9 @@
1
1
  // Express middleware for hearald event tracking
2
2
  import express from "express";
3
- import log from "./logger.js";
4
- import { nowAsPstDate } from "../shared/utils.js";
5
- import { writeEvent, closeEventFiles } from "./serverEvents.js";
6
- import { createAnalyticsRouter } from "./analytics.js";
7
3
 
8
- export async function logEvent(time, event) {
9
- return writeEvent(time, event);
10
- }
4
+ import hearaldConfiguration from "./configuration.js";
5
+ import log from "./logger.js";
6
+ import { writeEvent, closeEventFiles, VISIT_EVENT } from "./serverEvents.js";
11
7
 
12
8
  export async function shutDown() {
13
9
  await closeEventFiles();
@@ -29,11 +25,11 @@ export function eventEndpointMiddleware({ url = "/e", parseBody } = {}) {
29
25
  ({ e, u, p } = parseBody(req));
30
26
  } else {
31
27
  ({ e = null, u = null, p = {} } = req.body || {});
32
- if (e === "visit") {
28
+ if (e === VISIT_EVENT) {
33
29
  p.ip = req.ip;
34
30
  }
35
31
  }
36
- await logEvent(nowAsPstDate(), { e, u, p });
32
+ await writeEvent(hearaldConfiguration.nowFn(), { e, u, p });
37
33
  } catch (err) {
38
34
  // eslint-disable-next-line no-console
39
35
  log.error("error writing event to event log", err);
@@ -42,3 +38,57 @@ export function eventEndpointMiddleware({ url = "/e", parseBody } = {}) {
42
38
  });
43
39
  return router;
44
40
  }
41
+
42
+ export async function trackServerEvent({
43
+ event,
44
+ time = null,
45
+ userId,
46
+ params = {},
47
+ }) {
48
+ await writeEvent(time || hearaldConfiguration.nowFn(), {
49
+ e: event,
50
+ u: userId,
51
+ p: params,
52
+ });
53
+ }
54
+
55
+ export async function trackServerError({
56
+ error,
57
+ userId = null,
58
+ extraParams = {},
59
+ }) {
60
+ let message = "";
61
+ let stack = "";
62
+ if (error instanceof Error) {
63
+ message = error.message;
64
+ stack = error.stack || "";
65
+ } else if (typeof error === "string") {
66
+ message = error;
67
+ } else if (error && typeof error === "object") {
68
+ message = error.message || JSON.stringify(error);
69
+ stack = error.stack || "";
70
+ }
71
+ const params = {
72
+ message,
73
+ stack,
74
+ ...extraParams,
75
+ };
76
+ await writeEvent(hearaldConfiguration.nowFn(), {
77
+ e: ERROR_EVENT,
78
+ u: userId,
79
+ p: params,
80
+ });
81
+ }
82
+
83
+ // Express error-handling middleware
84
+ export function errorTrackingMiddleware({}) {
85
+ // eslint-disable-next-line no-unused-vars
86
+ return function (err, req, res, next) {
87
+ trackServerError({
88
+ error: err,
89
+ userId: hearaldConfiguration.getUserId(req),
90
+ extraParams: { url: req.originalUrl, method: req.method, ip: req.ip },
91
+ });
92
+ next(err);
93
+ };
94
+ }
@@ -1,7 +1,10 @@
1
1
  import { readFile, open as openFile, mkdir } from "fs/promises";
2
2
 
3
+ import hearaldConfiguration from "./configuration.js";
3
4
  import log from "./logger.js";
4
- import { iso } from "../shared/utils.js";
5
+
6
+ export const VISIT_EVENT = "visit";
7
+ export const ERROR_EVENT = "error";
5
8
 
6
9
  const eventFilePromises = new Map();
7
10
  const lastWriteTimeouts = new Map();
@@ -11,14 +14,14 @@ export const openEventFile = async (fileName) => {
11
14
  return openFile(`events/${fileName}.log`, "a");
12
15
  };
13
16
 
14
- export const writeEvent = async (time, event, isoFn = iso) => {
17
+ export const writeEvent = async (time, event) => {
15
18
  let fileName;
16
19
 
17
- const dateString = isoFn(time);
20
+ const dateString = hearaldConfiguration.isoFn(time);
18
21
 
19
- if (event.e === "visit") {
22
+ if (event.e === VISIT_EVENT) {
20
23
  fileName = `${dateString}-visits`;
21
- } else if (event.e === "error") {
24
+ } else if (event.e === ERROR_EVENT) {
22
25
  fileName = `${dateString}-errors`;
23
26
  } else {
24
27
  fileName = dateString;
@@ -91,12 +94,12 @@ export const readEventsFromFile = async (fileName) => {
91
94
  }
92
95
  };
93
96
 
94
- export const loadEventsOverWindow = async (d, n, fileSuffix, isoFn = iso) => {
97
+ export const loadEventsOverWindow = async (d, n, fileSuffix) => {
95
98
  let events = [];
96
99
  for (let i = 0; i < n; i++) {
97
100
  const newDate = new Date(d);
98
101
  newDate.setDate(d.getDate() - i);
99
- let fileName = isoFn(newDate);
102
+ let fileName = hearaldConfiguration.isoFn(newDate);
100
103
  if (fileSuffix) {
101
104
  fileName += `-${fileSuffix}`;
102
105
  }
@@ -10,8 +10,8 @@ export const pacificTimeFormatter = new Intl.DateTimeFormat("en-US", {
10
10
  export const pacificTimeOffsetHours = (date) =>
11
11
  pacificTimeFormatter.format(date || new Date()).endsWith("PDT") ? -7 : -8;
12
12
 
13
- export const dateStringToPstDate = (dateString) => {
14
- const rawDate = dateString ? new Date(dateString) : new Date();
13
+ export const nowAsPstDate = () => {
14
+ const rawDate = new Date();
15
15
  const offset = pacificTimeOffsetHours(rawDate);
16
16
  const pstDate = new Date();
17
17
  pstDate.setUTCFullYear(
@@ -19,12 +19,10 @@ export const dateStringToPstDate = (dateString) => {
19
19
  rawDate.getUTCMonth(),
20
20
  rawDate.getUTCDate()
21
21
  );
22
- pstDate.setUTCHours(-1 * offset, 0, 0, 0);
22
+ pstDate.setUTCHours(-1 * offset);
23
23
  return pstDate;
24
24
  };
25
25
 
26
- export const nowAsPstDate = () => dateStringToPstDate();
27
-
28
26
  export function omitProperties(object, ...keys) {
29
27
  let rest = object;
30
28
  keys.forEach((key) => {
@@ -1,47 +0,0 @@
1
- // hearald/src/serverTrackError.js
2
- // Server-side utility to track errors
3
-
4
- export async function trackServerError({
5
- error,
6
- logEvent,
7
- userId = null,
8
- extraParams = {},
9
- nowFn = () => new Date(),
10
- }) {
11
- let message = "";
12
- let stack = "";
13
- if (error instanceof Error) {
14
- message = error.message;
15
- stack = error.stack || "";
16
- } else if (typeof error === "string") {
17
- message = error;
18
- } else if (error && typeof error === "object") {
19
- message = error.message || JSON.stringify(error);
20
- stack = error.stack || "";
21
- }
22
- const params = {
23
- message,
24
- stack,
25
- ...extraParams,
26
- };
27
- await logEvent(nowFn(), { e: "error", u: userId, p: params });
28
- }
29
-
30
- // Express error-handling middleware
31
- export function errorTrackingMiddleware({
32
- logEvent,
33
- nowFn = () => new Date(),
34
- userIdExtractor = (req) => req.user?.id,
35
- }) {
36
- // eslint-disable-next-line no-unused-vars
37
- return function (err, req, res, next) {
38
- trackServerError({
39
- error: err,
40
- logEvent,
41
- userId: userIdExtractor(req),
42
- extraParams: { url: req.originalUrl, method: req.method, ip: req.ip },
43
- nowFn,
44
- });
45
- next(err);
46
- };
47
- }