hearald 0.0.1 → 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/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
+ }
@@ -0,0 +1,381 @@
1
+ :root {
2
+ --global-max-width: 480px;
3
+
4
+ --border-radius: 4px;
5
+
6
+ /* Fonts */
7
+ --font-sans-serif: Raleway, sans-serif;
8
+
9
+ /* Colors: start point https://color.adobe.com/Architecture-color-theme-20477657 */
10
+ --color-black: hsla(214, 94%, 7%, 1);
11
+ --color-dark: hsla(209, 37%, 18%, 1);
12
+ --color-mid: hsla(196, 34%, 48%, 1);
13
+ --color-light: hsla(191, 20%, 68%, 1);
14
+ --color-accent: hsla(38, 85%, 64%, 1);
15
+ --color-white: hsla(38, 38%, 90%, 1);
16
+
17
+ --color-text: var(--color-white);
18
+ --color-label: var(--color-light);
19
+
20
+ --color-link: var(--color-accent);
21
+ --color-link-hover: var(--color-light);
22
+ --color-title: var(--color-accent);
23
+
24
+ --color-bg: var(--color-black);
25
+ --color-menu: var(--color-dark);
26
+
27
+ --color-input-bg: var(--color-white);
28
+ --color-input-text: var(--color-dark);
29
+
30
+ --color-button-bg: var(--color-mid);
31
+ --color-button-bg-hover: var(--color-light);
32
+ --color-button-text: var(--color-black);
33
+
34
+ --color-logo-sign: var(--color-success);
35
+
36
+ --color-success: #127d44;
37
+ --color-error: #ad2e41;
38
+
39
+ --animation-plop: plop 300ms ease-out forwards;
40
+
41
+ --hero-box-shadow: 0 0 0.75em rgba(0, 0, 0, 0.75);
42
+
43
+ --carousel-hover-transition: 80ms ease-out transform;
44
+ }
45
+
46
+ @media (prefers-color-scheme: light) {
47
+ :root {
48
+ --color-text: var(--color-dark);
49
+ --color-label: hsla(196, 34%, 38%, 1);
50
+
51
+ --color-link: var(--color-mid);
52
+ --color-link-hover: var(--color-light);
53
+ --color-title: var(--color-dark);
54
+
55
+ --color-bg: #fff;
56
+ --color-menu: #fff;
57
+
58
+ --color-input-bg: var(--color-white);
59
+ --color-input-text: var(--color-dark);
60
+
61
+ --color-button-bg: var(--color-mid);
62
+ --color-button-bg-hover: var(--color-light);
63
+ --color-button-text: var(--color-black);
64
+
65
+ --color-logo-sign: var(--color-success);
66
+ }
67
+ }
68
+
69
+ html {
70
+ height: 100%;
71
+ }
72
+
73
+ * {
74
+ box-sizing: border-box;
75
+ }
76
+
77
+ body {
78
+ font-family: var(--font-sans-serif);
79
+ line-height: 1.6;
80
+ font-size: 14px;
81
+ background: var(--color-bg);
82
+ margin: 0;
83
+ height: 100%;
84
+ -webkit-text-size-adjust: none;
85
+ text-size-adjust: none;
86
+ color: var(--color-text);
87
+ }
88
+
89
+ /* need this to override the user style sheet in chrome, which has higher
90
+ specificity than inheriting the font style tag from the body */
91
+ input {
92
+ font-family: var(--font-sans-serif);
93
+ }
94
+
95
+ body.lock-zoom {
96
+ touch-action: none;
97
+ overscroll-behavior-y: none;
98
+ }
99
+
100
+ h1 {
101
+ font-family: var(--font-sans-serif);
102
+ font-size: 2.5em;
103
+ margin: 0;
104
+ }
105
+
106
+ h2 {
107
+ font-family: var(--font-sans-serif);
108
+ font-size: 1.5em;
109
+ margin: 0;
110
+ }
111
+
112
+ td,
113
+ th {
114
+ padding: 0 0.5em;
115
+ }
116
+
117
+ th {
118
+ text-align: left;
119
+ font-size: 0.8em;
120
+ }
121
+
122
+ @media screen and (max-width: 640px) {
123
+ body {
124
+ font-size: 12px;
125
+ font-feature-settings: 'lnum';
126
+ }
127
+ }
128
+
129
+ .hidden {
130
+ display: none !important;
131
+ }
132
+
133
+ .header__logo,
134
+ .header__button {
135
+ width: 2rem;
136
+ height: 2rem;
137
+ font-size: 2rem;
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: center;
141
+ padding: 0;
142
+ text-decoration: none;
143
+ }
144
+
145
+ .header__button:hover {
146
+ text-decoration: none;
147
+ }
148
+
149
+ .header__button--has-menu {
150
+ cursor: pointer;
151
+ }
152
+
153
+ /* ---- */
154
+
155
+ .analytics__header {
156
+ grid-area: header;
157
+ padding: 2em;
158
+ background-color: var(--color-dark);
159
+ display: flex;
160
+ gap: 2em;
161
+ align-items: center;
162
+ grid-template-columns: auto auto auto auto 1fr;
163
+ }
164
+
165
+ .analytics {
166
+ --analytics-grid-columns: 4;
167
+
168
+ display: grid;
169
+ grid-template-columns: repeat(var(--analytics-grid-columns), 1fr);
170
+ grid-gap: 0.5em;
171
+ padding: 1em;
172
+ }
173
+
174
+ .analytics__title {
175
+ grid-column: span var(--analytics-grid-columns);
176
+ }
177
+
178
+ .analytics__tile {
179
+ display: flex;
180
+ /* flex: 1fr 5fr; */
181
+ flex: 1 1 auto;
182
+ flex-direction: column;
183
+ border: 1px solid var(--color-accent);
184
+ }
185
+
186
+ .analytics__tile--end-row {
187
+ border: 0;
188
+ grid-column: span var(--analytics-grid-columns);
189
+ }
190
+
191
+ .analytics__tile__header {
192
+ padding: 0.2em 0.5em;
193
+ border-bottom: 1px solid var(--color-accent);
194
+ }
195
+ .analytics__tile__value {
196
+ padding: 0.2em 0.5em;
197
+ margin: 0 auto;
198
+ font-size: 2.5em;
199
+ font-weight: 600;
200
+ }
201
+
202
+ .analytics__menu {
203
+ grid-column: 1 /1;
204
+ display: flex;
205
+ gap: 1em;
206
+ align-items: center;
207
+ }
208
+
209
+ .analytics__menu > label {
210
+ display: flex;
211
+ gap: 0.5em;
212
+ align-items: center;
213
+ }
214
+
215
+ .analytics__menu > button {
216
+ font-size: 9px;
217
+ line-height: 9px;
218
+ height: 18px;
219
+ }
220
+
221
+ .bar-chart {
222
+ grid-column: 1/-1;
223
+ height: 200px;
224
+ display: grid;
225
+ grid-auto-flow: column;
226
+ grid-auto-columns: 1fr;
227
+ grid-template-columns: 1fr;
228
+ gap: 2px;
229
+ position: relative;
230
+ align-items: end;
231
+ margin-top: 1em;
232
+ padding: 0 1.5em 0 0;
233
+ border-bottom: 1px solid var(--color-light);
234
+ }
235
+ .bar-chart__bar {
236
+ background: var(--color-mid);
237
+ height: calc(var(--bar-height) * 100%);
238
+ position: relative;
239
+ }
240
+ .bar-chart__bar__label {
241
+ display: none;
242
+ position: absolute;
243
+ bottom: calc(100% + 0.75em);
244
+ left: 50%;
245
+ transform: translateX(-50%);
246
+ z-index: 2;
247
+ background: var(--color-label);
248
+ padding: 0.25em 0.5em;
249
+ border-radius: 0.25em;
250
+ text-align: center;
251
+ white-space: nowrap;
252
+ word-break: keep-all;
253
+ }
254
+ .bar-chart__bar__label:after {
255
+ content: '';
256
+ width: 12px;
257
+ height: 12px;
258
+ background: var(--color-label);
259
+ position: absolute;
260
+ margin: auto;
261
+ left: 0;
262
+ right: 0;
263
+ bottom: 0;
264
+ transform: translateY(50%) rotate(45deg);
265
+ z-index: 1;
266
+ }
267
+ .bar-chart__bar:hover {
268
+ background: var(--color-light);
269
+ }
270
+ .bar-chart__bar:hover > .bar-chart__bar__label {
271
+ display: block;
272
+ }
273
+ .bar-chart__empty {
274
+ place-self: center;
275
+ }
276
+ .bar-chart__scale {
277
+ align-self: start;
278
+ border-top: 1px solid var(--color-light);
279
+ border-right: 1px solid var(--color-light);
280
+ height: 100%;
281
+ padding-right: 0.2em;
282
+ text-align: right;
283
+ }
284
+
285
+ .analytics__tile__histogram {
286
+ list-style-type: none;
287
+ padding: 0;
288
+ margin: 0;
289
+ display: grid;
290
+ grid-template-columns: auto 1fr;
291
+ gap: 1em;
292
+ grid-column: 1/-1;
293
+ min-height: 10em;
294
+ max-height: 20em;
295
+ overflow: auto;
296
+ align-items: start;
297
+ grid-auto-rows: max-content;
298
+ padding: 0.5em 0.5em;
299
+ }
300
+ .analytics__tile__histogram__value {
301
+ text-align: right;
302
+ }
303
+ .analytics__tile__histogram__row {
304
+ display: grid;
305
+ grid-template-columns: subgrid;
306
+ grid-column: 1/-1;
307
+ align-self: start;
308
+ }
309
+ .analytics__tile__histogram__label {
310
+ position: relative;
311
+ background-image: linear-gradient(
312
+ to right,
313
+ var(--color-mid) calc(var(--bar-width) * 100%),
314
+ transparent calc(var(--bar-width) * 100%)
315
+ );
316
+ background-size: 100%;
317
+ padding: 0 0.5em;
318
+ min-width: 0;
319
+ min-height: 0;
320
+ }
321
+
322
+ .analytics__tile--span-2 {
323
+ grid-column: span 2;
324
+ }
325
+
326
+ @media screen and (max-width: 640px) {
327
+ .analytics {
328
+ --analytics-grid-columns: 2;
329
+ }
330
+
331
+ .analytics__tile--end-row {
332
+ display: none !important;
333
+ }
334
+
335
+ .analytics__tile--span-2 {
336
+ grid-column: span 1;
337
+ }
338
+ }
339
+
340
+ @media screen and (max-width: 380px) {
341
+ .analytics {
342
+ --analytics-grid-columns: 1;
343
+ }
344
+
345
+ .analytics__header__input {
346
+ width: 35%;
347
+ }
348
+
349
+ .analytics__menu > label {
350
+ display: grid;
351
+ }
352
+
353
+ .bar-chart {
354
+ grid-auto-flow: row;
355
+ height: auto;
356
+ border-bottom: 0;
357
+ border-left: 1px solid var(--color-light);
358
+ padding: 0;
359
+ }
360
+ .bar-chart__scale {
361
+ height: auto;
362
+ width: 100%;
363
+ border-top: 0;
364
+ border-bottom: 1px solid var(--color-light);
365
+ border-right: 1px solid var(--color-light);
366
+ }
367
+ .bar-chart__bar {
368
+ height: 0.5em;
369
+ width: calc(var(--bar-height) * 100%);
370
+ }
371
+ .bar-chart__bar__label {
372
+ left: 0.5em;
373
+ transform: translateX(0);
374
+ }
375
+ .bar-chart__bar__label:after {
376
+ left: 3px;
377
+ right: auto;
378
+ bottom: 0;
379
+ transform: translateY(50%) rotate(45deg);
380
+ }
381
+ }
@@ -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