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/README.md +6 -16
- package/dist/analytics.html +938 -0
- package/dist/index.js +537 -0
- package/package.json +1 -1
- package/rollup.config.js +2 -2
- package/src/client.js +105 -0
- package/src/index.js +25 -0
- package/src/public/css/analytics.css +381 -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/css/analytics.css → dist/index.css} +0 -0
- /package/{public → src/public}/analytics.html +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/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
|
+
}
|
|
@@ -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
|