hearald 0.0.1

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/.eslintrc ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "env": {
3
+ "browser": true,
4
+ "node": true,
5
+ "es2020": true
6
+ },
7
+ "parserOptions": {
8
+ "ecmaVersion": 2020,
9
+ "sourceType": "module"
10
+ },
11
+ "globals": {
12
+ "Intl": "readonly"
13
+ }
14
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Andrew Pariser
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # hearald
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
19
+
20
+ ## Usage
21
+
22
+ ### Tracking User Visits (Client)
23
+
24
+ ```js
25
+ import { trackVisit } from "hearald/client/trackVisit.js";
26
+ trackVisit({ userId: "user-123" });
27
+ ```
28
+
29
+ ### Tracking Errors (Client)
30
+
31
+ ```js
32
+ import { trackError } from "hearald/client/trackError.js";
33
+ try {
34
+ // ...code...
35
+ } catch (err) {
36
+ trackError(err);
37
+ }
38
+ ```
39
+
40
+ ### Tracking Errors (Server)
41
+
42
+ ```js
43
+ import {
44
+ trackServerError,
45
+ errorTrackingMiddleware,
46
+ } from "hearald/server/serverTrackError.js";
47
+ app.use(errorTrackingMiddleware);
48
+ ```
49
+
50
+ ### Analytics Helpers
51
+
52
+ ```js
53
+ import {
54
+ iso,
55
+ getData,
56
+ fetchChartData,
57
+ fetchData,
58
+ numFormatter,
59
+ } from "hearald/shared/analyticsClientHelpers.js";
60
+ ```
61
+
62
+ ### Admin Stats Endpoint
63
+
64
+ The adminStatsEndpoint has been removed. For admin statistics, use the analytics router provided by hearald/server/analytics.js.
65
+ Example:
66
+
67
+ ```js
68
+ import { createAnalyticsRouter } from 'hearald/server/analytics.js';
69
+ app.use('/admin/stats', createAnalyticsRouter({...}));
70
+ ```
71
+
72
+ ### Admin Stats Client Fetcher
73
+
74
+ ```js
75
+ import { fetchAdminStats } from "hearald/client/adminStatsClient.js";
76
+ const stats = await fetchAdminStats();
77
+ ```
78
+
79
+ ## Roadmap
80
+
81
+ See TODO.md for next steps and roadmap.
package/TODO.md ADDED
@@ -0,0 +1,34 @@
1
+ # hearald TODO
2
+
3
+ ## Status (August 2025)
4
+
5
+ - Event bus/evented logic, event file helpers, emit logic, admin stats, analytics helpers, and error tracking utilities have all been extracted and modularized.
6
+ - hearald now uses client/server/shared subfolders and has a working .eslintrc for Intl.
7
+ - Lint passes and all functionality is verified.
8
+ - Migration of all helpers to client/server/shared is nearly complete.
9
+
10
+ ## Next Steps for hearald
11
+
12
+ 1. **Tracking Custom Events**
13
+
14
+ - Make the event schema/config easy to extend (document how to define custom events)
15
+ - Provide a clear API for emitting custom events (e.g., `trackEvent('my_event', {...})`)
16
+
17
+ 2. **Generating Statistical Summary Views**
18
+
19
+ - Make stat definitions configurable (allow user to provide their own stat_definition.json)
20
+ - Expose a summary API (e.g., `generateStats(endDate, timeWindow)`)
21
+ - Document how to add new metrics and computed stats
22
+
23
+ 3. **Admin UI to See Stats**
24
+ - Provide a basic, embeddable React/Preact/Vue component for admin stats (or a template)
25
+ - Document how to use the endpoint and client fetcher in any admin UI
26
+ - Optionally, provide a standalone admin page as part of the package
27
+
28
+ ## General Improvements
29
+
30
+ - Write clear documentation and usage examples
31
+ - Add configuration options for stat definitions and event types
32
+ - Provide ready-to-use middleware and client helpers for common event types
33
+ - Package a basic admin UI or template
34
+ - Test in a clean, new project to validate ease of adoption
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "hearald",
3
+ "version": "0.0.1",
4
+ "description": "A single server drop-in file system based metrics and analytics library.",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "rollup --config"
9
+ },
10
+ "keywords": [
11
+ "herald",
12
+ "analytics"
13
+ ],
14
+ "author": "pariser",
15
+ "homepage": "https://github.com/pariser/hearald",
16
+ "repository": "https://github.com/pariser/hearald",
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "bussin-js": "^1.0.1",
20
+ "chalk": "^4.1.0",
21
+ "express": "^4.17.1"
22
+ },
23
+ "devDependencies": {
24
+ "@rollup/plugin-commonjs": "^15.1.0",
25
+ "@rollup/plugin-json": "^4.1.0",
26
+ "@rollup/plugin-node-resolve": "^13.3.0",
27
+ "@rollup/plugin-replace": "^2.3.3",
28
+ "@web/rollup-plugin-html": "^2.3.0",
29
+ "eslint": "^8.27.0",
30
+ "eslint-plugin-import": "^2.26.0",
31
+ "eslint-plugin-jsx-a11y": "^6.6.1",
32
+ "eslint-plugin-react": "^7.31.10",
33
+ "eslint-plugin-react-hooks": "^4.6.0",
34
+ "htm": "^3.0.3",
35
+ "preact": "^10.3.3",
36
+ "prettier": "2.2.1",
37
+ "rollup": "^2.79.2",
38
+ "rollup-plugin-embed-css": "^1.0.26",
39
+ "rollup-plugin-generate-html-template": "^1.7.0",
40
+ "rollup-plugin-output-manifest": "^2.0.0",
41
+ "rollup-plugin-postcss": "^4.0.2",
42
+ "rollup-plugin-terser": "^7.0.2",
43
+ "style-inject": "^0.3.0"
44
+ }
45
+ }
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Analytics</title>
6
+
7
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
8
+ <meta
9
+ name="viewport"
10
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover, user-scalable=no"
11
+ />
12
+
13
+ <link rel="stylesheet" href="./css/analytics.css" />
14
+ </head>
15
+
16
+ <body>
17
+ <div id="analytics-root"></div>
18
+ <script src="./js/index.js" defer></script>
19
+ </body>
20
+ </html>
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "env": {
3
+ "browser": true,
4
+ "es2020": true
5
+ },
6
+ "globals": {
7
+ "INJECTED__DATE": "readonly"
8
+ }
9
+ }