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/dist/index.js ADDED
@@ -0,0 +1,537 @@
1
+ (function () {
2
+ 'use strict';
3
+
4
+ var n$1,l,u,i,r,o,e$1,f,c,s,a,p={},v=[],y=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,w=Array.isArray;function d(n,l){for(var u in l)n[u]=l[u];return n}function g(n){n&&n.parentNode&&n.parentNode.removeChild(n);}function _(l,u,t){var i,r,o,e={};for(o in u)"key"==o?i=u[o]:"ref"==o?r=u[o]:e[o]=u[o];if(arguments.length>2&&(e.children=arguments.length>3?n$1.call(arguments,2):t),"function"==typeof l&&null!=l.defaultProps)for(o in l.defaultProps)void 0===e[o]&&(e[o]=l.defaultProps[o]);return m$1(l,e,i,r,null)}function m$1(n,t,i,r,o){var e={type:n,props:t,key:i,ref:r,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:null==o?++u:o,__i:-1,__u:0};return null==o&&null!=l.vnode&&l.vnode(e),e}function k(n){return n.children}function x(n,l){this.props=n,this.context=l;}function S(n,l){if(null==l)return n.__?S(n.__,n.__i+1):null;for(var u;l<n.__k.length;l++)if(null!=(u=n.__k[l])&&null!=u.__e)return u.__e;return "function"==typeof n.type?S(n):null}function C(n){var l,u;if(null!=(n=n.__)&&null!=n.__c){for(n.__e=n.__c.base=null,l=0;l<n.__k.length;l++)if(null!=(u=n.__k[l])&&null!=u.__e){n.__e=n.__c.base=u.__e;break}return C(n)}}function M(n){(!n.__d&&(n.__d=!0)&&i.push(n)&&!$.__r++||r!=l.debounceRendering)&&((r=l.debounceRendering)||o)($);}function $(){for(var n,u,t,r,o,f,c,s=1;i.length;)i.length>s&&i.sort(e$1),n=i.shift(),s=i.length,n.__d&&(t=void 0,r=void 0,o=(r=(u=n).__v).__e,f=[],c=[],u.__P&&((t=d({},r)).__v=r.__v+1,l.vnode&&l.vnode(t),O(u.__P,t,r,u.__n,u.__P.namespaceURI,32&r.__u?[o]:null,f,null==o?S(r):o,!!(32&r.__u),c),t.__v=r.__v,t.__.__k[t.__i]=t,N(f,t,c),r.__e=r.__=null,t.__e!=o&&C(t)));$.__r=0;}function I(n,l,u,t,i,r,o,e,f,c,s){var a,h,y,w,d,g,_,m=t&&t.__k||v,b=l.length;for(f=P(u,l,m,f,b),a=0;a<b;a++)null!=(y=u.__k[a])&&(h=-1==y.__i?p:m[y.__i]||p,y.__i=a,g=O(n,y,h,i,r,o,e,f,c,s),w=y.__e,y.ref&&h.ref!=y.ref&&(h.ref&&B(h.ref,null,y),s.push(y.ref,y.__c||w,y)),null==d&&null!=w&&(d=w),(_=!!(4&y.__u))||h.__k===y.__k?f=A(y,f,n,_):"function"==typeof y.type&&void 0!==g?f=g:w&&(f=w.nextSibling),y.__u&=-7);return u.__e=d,f}function P(n,l,u,t,i){var r,o,e,f,c,s=u.length,a=s,h=0;for(n.__k=new Array(i),r=0;r<i;r++)null!=(o=l[r])&&"boolean"!=typeof o&&"function"!=typeof o?(f=r+h,(o=n.__k[r]="string"==typeof o||"number"==typeof o||"bigint"==typeof o||o.constructor==String?m$1(null,o,null,null,null):w(o)?m$1(k,{children:o},null,null,null):null==o.constructor&&o.__b>0?m$1(o.type,o.props,o.key,o.ref?o.ref:null,o.__v):o).__=n,o.__b=n.__b+1,e=null,-1!=(c=o.__i=L(o,u,f,a))&&(a--,(e=u[c])&&(e.__u|=2)),null==e||null==e.__v?(-1==c&&(i>s?h--:i<s&&h++),"function"!=typeof o.type&&(o.__u|=4)):c!=f&&(c==f-1?h--:c==f+1?h++:(c>f?h--:h++,o.__u|=4))):n.__k[r]=null;if(a)for(r=0;r<s;r++)null!=(e=u[r])&&0==(2&e.__u)&&(e.__e==t&&(t=S(e)),D(e,e));return t}function A(n,l,u,t){var i,r;if("function"==typeof n.type){for(i=n.__k,r=0;i&&r<i.length;r++)i[r]&&(i[r].__=n,l=A(i[r],l,u,t));return l}n.__e!=l&&(t&&(l&&n.type&&!l.parentNode&&(l=S(n)),u.insertBefore(n.__e,l||null)),l=n.__e);do{l=l&&l.nextSibling;}while(null!=l&&8==l.nodeType);return l}function L(n,l,u,t){var i,r,o,e=n.key,f=n.type,c=l[u],s=null!=c&&0==(2&c.__u);if(null===c&&null==n.key||s&&e==c.key&&f==c.type)return u;if(t>(s?1:0))for(i=u-1,r=u+1;i>=0||r<l.length;)if(null!=(c=l[o=i>=0?i--:r++])&&0==(2&c.__u)&&e==c.key&&f==c.type)return o;return -1}function T(n,l,u){"-"==l[0]?n.setProperty(l,null==u?"":u):n[l]=null==u?"":"number"!=typeof u||y.test(l)?u:u+"px";}function j(n,l,u,t,i){var r,o;n:if("style"==l)if("string"==typeof u)n.style.cssText=u;else {if("string"==typeof t&&(n.style.cssText=t=""),t)for(l in t)u&&l in u||T(n.style,l,"");if(u)for(l in u)t&&u[l]==t[l]||T(n.style,l,u[l]);}else if("o"==l[0]&&"n"==l[1])r=l!=(l=l.replace(f,"$1")),o=l.toLowerCase(),l=o in n||"onFocusOut"==l||"onFocusIn"==l?o.slice(2):l.slice(2),n.l||(n.l={}),n.l[l+r]=u,u?t?u.u=t.u:(u.u=c,n.addEventListener(l,r?a:s,r)):n.removeEventListener(l,r?a:s,r);else {if("http://www.w3.org/2000/svg"==i)l=l.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if("width"!=l&&"height"!=l&&"href"!=l&&"list"!=l&&"form"!=l&&"tabIndex"!=l&&"download"!=l&&"rowSpan"!=l&&"colSpan"!=l&&"role"!=l&&"popover"!=l&&l in n)try{n[l]=null==u?"":u;break n}catch(n){}"function"==typeof u||(null==u||!1===u&&"-"!=l[4]?n.removeAttribute(l):n.setAttribute(l,"popover"==l&&1==u?"":u));}}function F(n){return function(u){if(this.l){var t=this.l[u.type+n];if(null==u.t)u.t=c++;else if(u.t<t.u)return;return t(l.event?l.event(u):u)}}}function O(n,u,t,i,r,o,e,f,c,s){var a,h,p,v,y,_,m,b,S,C,M,$,P,A,H,L,T,j=u.type;if(null!=u.constructor)return null;128&t.__u&&(c=!!(32&t.__u),o=[f=u.__e=t.__e]),(a=l.__b)&&a(u);n:if("function"==typeof j)try{if(b=u.props,S="prototype"in j&&j.prototype.render,C=(a=j.contextType)&&i[a.__c],M=a?C?C.props.value:a.__:i,t.__c?m=(h=u.__c=t.__c).__=h.__E:(S?u.__c=h=new j(b,M):(u.__c=h=new x(b,M),h.constructor=j,h.render=E),C&&C.sub(h),h.props=b,h.state||(h.state={}),h.context=M,h.__n=i,p=h.__d=!0,h.__h=[],h._sb=[]),S&&null==h.__s&&(h.__s=h.state),S&&null!=j.getDerivedStateFromProps&&(h.__s==h.state&&(h.__s=d({},h.__s)),d(h.__s,j.getDerivedStateFromProps(b,h.__s))),v=h.props,y=h.state,h.__v=u,p)S&&null==j.getDerivedStateFromProps&&null!=h.componentWillMount&&h.componentWillMount(),S&&null!=h.componentDidMount&&h.__h.push(h.componentDidMount);else {if(S&&null==j.getDerivedStateFromProps&&b!==v&&null!=h.componentWillReceiveProps&&h.componentWillReceiveProps(b,M),!h.__e&&null!=h.shouldComponentUpdate&&!1===h.shouldComponentUpdate(b,h.__s,M)||u.__v==t.__v){for(u.__v!=t.__v&&(h.props=b,h.state=h.__s,h.__d=!1),u.__e=t.__e,u.__k=t.__k,u.__k.some(function(n){n&&(n.__=u);}),$=0;$<h._sb.length;$++)h.__h.push(h._sb[$]);h._sb=[],h.__h.length&&e.push(h);break n}null!=h.componentWillUpdate&&h.componentWillUpdate(b,h.__s,M),S&&null!=h.componentDidUpdate&&h.__h.push(function(){h.componentDidUpdate(v,y,_);});}if(h.context=M,h.props=b,h.__P=n,h.__e=!1,P=l.__r,A=0,S){for(h.state=h.__s,h.__d=!1,P&&P(u),a=h.render(h.props,h.state,h.context),H=0;H<h._sb.length;H++)h.__h.push(h._sb[H]);h._sb=[];}else do{h.__d=!1,P&&P(u),a=h.render(h.props,h.state,h.context),h.state=h.__s;}while(h.__d&&++A<25);h.state=h.__s,null!=h.getChildContext&&(i=d(d({},i),h.getChildContext())),S&&!p&&null!=h.getSnapshotBeforeUpdate&&(_=h.getSnapshotBeforeUpdate(v,y)),L=a,null!=a&&a.type===k&&null==a.key&&(L=V(a.props.children)),f=I(n,w(L)?L:[L],u,t,i,r,o,e,f,c,s),h.base=u.__e,u.__u&=-161,h.__h.length&&e.push(h),m&&(h.__E=h.__=null);}catch(n){if(u.__v=null,c||null!=o)if(n.then){for(u.__u|=c?160:128;f&&8==f.nodeType&&f.nextSibling;)f=f.nextSibling;o[o.indexOf(f)]=null,u.__e=f;}else {for(T=o.length;T--;)g(o[T]);z(u);}else u.__e=t.__e,u.__k=t.__k,n.then||z(u);l.__e(n,u,t);}else null==o&&u.__v==t.__v?(u.__k=t.__k,u.__e=t.__e):f=u.__e=q(t.__e,u,t,i,r,o,e,c,s);return (a=l.diffed)&&a(u),128&u.__u?void 0:f}function z(n){n&&n.__c&&(n.__c.__e=!0),n&&n.__k&&n.__k.forEach(z);}function N(n,u,t){for(var i=0;i<t.length;i++)B(t[i],t[++i],t[++i]);l.__c&&l.__c(u,n),n.some(function(u){try{n=u.__h,u.__h=[],n.some(function(n){n.call(u);});}catch(n){l.__e(n,u.__v);}});}function V(n){return "object"!=typeof n||null==n||n.__b&&n.__b>0?n:w(n)?n.map(V):d({},n)}function q(u,t,i,r,o,e,f,c,s){var a,h,v,y,d,_,m,b=i.props,k=t.props,x=t.type;if("svg"==x?o="http://www.w3.org/2000/svg":"math"==x?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),null!=e)for(a=0;a<e.length;a++)if((d=e[a])&&"setAttribute"in d==!!x&&(x?d.localName==x:3==d.nodeType)){u=d,e[a]=null;break}if(null==u){if(null==x)return document.createTextNode(k);u=document.createElementNS(o,x,k.is&&k),c&&(l.__m&&l.__m(t,e),c=!1),e=null;}if(null==x)b===k||c&&u.data==k||(u.data=k);else {if(e=e&&n$1.call(u.childNodes),b=i.props||p,!c&&null!=e)for(b={},a=0;a<u.attributes.length;a++)b[(d=u.attributes[a]).name]=d.value;for(a in b)if(d=b[a],"children"==a);else if("dangerouslySetInnerHTML"==a)v=d;else if(!(a in k)){if("value"==a&&"defaultValue"in k||"checked"==a&&"defaultChecked"in k)continue;j(u,a,null,d,o);}for(a in k)d=k[a],"children"==a?y=d:"dangerouslySetInnerHTML"==a?h=d:"value"==a?_=d:"checked"==a?m=d:c&&"function"!=typeof d||b[a]===d||j(u,a,d,b[a],o);if(h)c||v&&(h.__html==v.__html||h.__html==u.innerHTML)||(u.innerHTML=h.__html),t.__k=[];else if(v&&(u.innerHTML=""),I("template"==t.type?u.content:u,w(y)?y:[y],t,i,r,"foreignObject"==x?"http://www.w3.org/1999/xhtml":o,e,f,e?e[0]:i.__k&&S(i,0),c,s),null!=e)for(a=e.length;a--;)g(e[a]);c||(a="value","progress"==x&&null==_?u.removeAttribute("value"):null!=_&&(_!==u[a]||"progress"==x&&!_||"option"==x&&_!=b[a])&&j(u,a,_,b[a],o),a="checked",null!=m&&m!=u[a]&&j(u,a,m,b[a],o));}return u}function B(n,u,t){try{if("function"==typeof n){var i="function"==typeof n.__u;i&&n.__u(),i&&null==u||(n.__u=n(u));}else n.current=u;}catch(n){l.__e(n,t);}}function D(n,u,t){var i,r;if(l.unmount&&l.unmount(n),(i=n.ref)&&(i.current&&i.current!=n.__e||B(i,null,u)),null!=(i=n.__c)){if(i.componentWillUnmount)try{i.componentWillUnmount();}catch(n){l.__e(n,u);}i.base=i.__P=null;}if(i=n.__k)for(r=0;r<i.length;r++)i[r]&&D(i[r],u,t||"function"!=typeof n.type);t||g(n.__e),n.__c=n.__=n.__e=void 0;}function E(n,l,u){return this.constructor(n,u)}function G(u,t,i){var r,o,e,f;t==document&&(t=document.documentElement),l.__&&l.__(u,t),o=(r="function"==typeof i)?null:i&&i.__k||t.__k,e=[],f=[],O(t,u=(!r&&i||t).__k=_(k,null,[u]),o||p,p,t.namespaceURI,!r&&i?[i]:o?null:t.firstChild?n$1.call(t.childNodes):null,e,!r&&i?i:o?o.__e:t.firstChild,r,f),N(e,u,f);}n$1=v.slice,l={__e:function(n,l,u,t){for(var i,r,o;l=l.__;)if((i=l.__c)&&!i.__)try{if((r=i.constructor)&&null!=r.getDerivedStateFromError&&(i.setState(r.getDerivedStateFromError(n)),o=i.__d),null!=i.componentDidCatch&&(i.componentDidCatch(n,t||{}),o=i.__d),o)return i.__E=i}catch(l){n=l;}throw n}},u=0,x.prototype.setState=function(n,l){var u;u=null!=this.__s&&this.__s!=this.state?this.__s:this.__s=d({},this.state),"function"==typeof n&&(n=n(d({},u),this.props)),n&&d(u,n),null!=n&&this.__v&&(l&&this._sb.push(l),M(this));},x.prototype.forceUpdate=function(n){this.__v&&(this.__e=!0,n&&this.__h.push(n),M(this));},x.prototype.render=k,i=[],o="function"==typeof Promise?Promise.prototype.then.bind(Promise.resolve()):setTimeout,e$1=function(n,l){return n.__v.__b-l.__v.__b},$.__r=0,f=/(PointerCapture)$|Capture$/i,c=0,s=F(!1),a=F(!0);
5
+
6
+ var n=function(t,s,r,e){var u;s[0]=0;for(var h=1;h<s.length;h++){var p=s[h++],a=s[h]?(s[0]|=p?1:2,r[s[h++]]):s[++h];3===p?e[0]=a:4===p?e[1]=Object.assign(e[1]||{},a):5===p?(e[1]=e[1]||{})[s[++h]]=a:6===p?e[1][s[++h]]+=a+"":p?(u=t.apply(a,n(t,a,r,["",null])),e.push(u),a[0]?s[0]|=2:(s[h-2]=0,s[h]=u)):e.push(a);}return e},t=new Map;function e(s){var r=t.get(this);return r||(r=new Map,t.set(this,r)),(r=n(this,r.get(s)||(r.set(s,r=function(n){for(var t,s,r=1,e="",u="",h=[0],p=function(n){1===r&&(n||(e=e.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?h.push(0,n,e):3===r&&(n||e)?(h.push(3,n,e),r=2):2===r&&"..."===e&&n?h.push(4,n,0):2===r&&e&&!n?h.push(5,0,!0,e):r>=5&&((e||!n&&5===r)&&(h.push(r,0,e,s),r=6),n&&(h.push(r,n,0,s),r=6)),e="";},a=0;a<n.length;a++){a&&(1===r&&p(),p(a));for(var l=0;l<n[a].length;l++)t=n[a][l],1===r?"<"===t?(p(),h=[h],r=3):e+=t:4===r?"--"===e&&">"===t?(r=1,e=""):e=t+e[0]:u?t===u?u="":e+=t:'"'===t||"'"===t?u=t:">"===t?(p(),r=1):r&&("="===t?(r=5,s=e,e=""):"/"===t&&(r<5||">"===n[a][l+1])?(p(),3===r&&(h=h[0]),r=h,(h=h[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),r=2):e+=t),3===r&&"!--"===e&&(r=4,h=h[0]);}return p(),h}(s)),r),arguments,[])).length>1?r:r[0]}
7
+
8
+ var m=e.bind(_);
9
+
10
+ function iso(time) {
11
+ return [
12
+ time.getUTCFullYear(),
13
+ (time.getUTCMonth() + 1).toString().padStart(2, "0"),
14
+ time.getUTCDate().toString().padStart(2, "0"),
15
+ ].join("-");
16
+ }
17
+
18
+ /* START methods for fetching data from the server */
19
+
20
+ const dataCache = {};
21
+
22
+ // Fetches data from the server with caching
23
+ async function getData({ n, d, q, endpointBuilder, fetchStats }) {
24
+ const key = [n, d, q].join("_");
25
+ if (dataCache[key]) {
26
+ return dataCache[key];
27
+ }
28
+
29
+ const endpoint = endpointBuilder
30
+ ? endpointBuilder(n, d, q)
31
+ : `/data/${n}/${d}${q ? "?" + q : ""}`;
32
+
33
+ const data = await fetchStats(endpoint);
34
+ dataCache[key] = data;
35
+ return data;
36
+ }
37
+
38
+ // Fetches and builds chart data for a given field/context over a time window
39
+ async function fetchChartData({
40
+ numDays,
41
+ date,
42
+ chartContext,
43
+ chartField,
44
+ getDataFn,
45
+
46
+ onLoadProgress, // (i, n) => void
47
+ onLoadComplete, // (data, max) => void
48
+ }) {
49
+ const n = numDays;
50
+ const d = date;
51
+ const field = chartField;
52
+ const context = chartContext;
53
+
54
+ const [yyyy, mm, dd] = d.split("-");
55
+ const startDate = new Date(yyyy, mm - 1, dd);
56
+
57
+ let data = [];
58
+ let max = 1;
59
+
60
+ for (let i = 0; i < n; i++) {
61
+ let day = new Date(startDate);
62
+ day.setDate(startDate.getDate() - i);
63
+ onLoadProgress(i, n);
64
+
65
+ const json = await getDataFn({ n: 1, d: iso(day) });
66
+ const v = json.metrics[context][field] || 0;
67
+
68
+ data.unshift({ date: iso(day), value: v });
69
+ max = Math.max(max, v);
70
+ }
71
+
72
+ onLoadComplete(data, max);
73
+ }
74
+
75
+ // Fetches and sets the main stats data for a given date/range
76
+ async function fetchData({
77
+ numDays,
78
+ date,
79
+ getDataFn,
80
+ onLoadComplete, // (stats) => void
81
+ }) {
82
+ const n = numDays;
83
+ const d = date;
84
+ const stats = await getDataFn({ n, d });
85
+ onLoadComplete(stats);
86
+ }
87
+
88
+ /* END methods for fetching data from the server */
89
+
90
+ /* START helper methods */
91
+
92
+ const getJson = async (url) => {
93
+ const response = await fetch(url, {
94
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
95
+ });
96
+ return response.json();
97
+ };
98
+
99
+ const deepGet = (obj, path) => {
100
+ const parts = path.split(".");
101
+ let current = obj;
102
+ for (let i = 0; i < parts.length; i += 1) {
103
+ if (current == null) return undefined;
104
+ current = current[parts[i]];
105
+ }
106
+ return current;
107
+ };
108
+
109
+ function numFormatter(n) {
110
+ if (typeof Intl !== "undefined" && Intl.NumberFormat) {
111
+ return new Intl.NumberFormat().format(n);
112
+ }
113
+ return n;
114
+ }
115
+
116
+ /* END helper methods */
117
+
118
+ const prettyNumericFormatter = (val) => {
119
+ const digits = Math.log10(val);
120
+ let truncDigits = 0;
121
+ let decimalPlaces = 0;
122
+ let suffix = "";
123
+ if (digits > 8) {
124
+ truncDigits = 9;
125
+ suffix = "b";
126
+ decimalPlaces = 2;
127
+ } else if (digits > 5) {
128
+ truncDigits = 6;
129
+ suffix = "m";
130
+ decimalPlaces = 2;
131
+ } else if (digits > 3) {
132
+ truncDigits = 3;
133
+ suffix = "k";
134
+ decimalPlaces = 2;
135
+ }
136
+
137
+ const numeric = Math.floor(val / 10 ** truncDigits);
138
+ if (decimalPlaces) {
139
+ const fractional =
140
+ (val % 10 ** decimalPlaces) % 10 ** (truncDigits - decimalPlaces);
141
+ return `${numeric}.${fractional}${suffix}`;
142
+ } else {
143
+ return `${numeric}${suffix}`;
144
+ }
145
+ };
146
+
147
+ const percentFormatter = (val) =>
148
+ `${(Math.round(val * 100) / 100).toFixed(2)}%`;
149
+
150
+ const plainFormatter = (val) => val;
151
+
152
+ const getFormattedValue = (value, percent = false, plain = false) => {
153
+ let formattedValue;
154
+ let formatter = prettyNumericFormatter;
155
+ if (percent) {
156
+ if (plain) {
157
+ console.error("Multiple formatters specified, using percent formatter");
158
+ }
159
+
160
+ formatter = percentFormatter;
161
+ } else if (plain) {
162
+ formatter = plainFormatter;
163
+ }
164
+
165
+ if (isNaN(value)) {
166
+ formattedValue = "(no data)";
167
+ } else {
168
+ formattedValue = formatter(value);
169
+ }
170
+
171
+ return formattedValue;
172
+ };
173
+
174
+ class Tile extends x {
175
+ render({
176
+ title,
177
+ value,
178
+ type,
179
+ percent = false,
180
+ plain = false,
181
+ endRow = false,
182
+ span = null,
183
+ }) {
184
+ return m`
185
+ <div
186
+ class="analytics__tile ${span ? `analytics__tile--span-${span}` : ""}"
187
+ >
188
+ <div class="analytics__tile__header">${title}</div>
189
+ ${type === "histogram"
190
+ ? m`
191
+ <!-- div class="analytics__tile__value" -->
192
+ <ul class="analytics__tile__histogram">
193
+ ${value && value.length
194
+ ? value.map(
195
+ ([label, count]) => m`
196
+ <li class="analytics__tile__histogram__row">
197
+ <div class="analytics__tile__histogram__value">
198
+ ${count}
199
+ </div>
200
+ <div
201
+ class="analytics__tile__histogram__label"
202
+ style="--bar-width:${count / (value[0]?.[1] || 1)}"
203
+ >
204
+ ${label}
205
+ </div>
206
+ </li>
207
+ `
208
+ )
209
+ : m`
210
+ <li class="analytics__tile__histogram__row">No data</li>
211
+ `}
212
+ </ul>
213
+ <!-- /div -->
214
+ `
215
+ : m`
216
+ <div class="analytics__tile__value" data-value=${value}>
217
+ ${getFormattedValue(value, percent, plain)}
218
+ </div>
219
+ `}
220
+ </div>
221
+ ${endRow &&
222
+ m` <div class="analytics__tile analytics__tile--end-row" /> `}
223
+ `;
224
+ }
225
+ }
226
+
227
+ const DATE = new Date().toISOString().split("T")[0];
228
+
229
+ const Logo = () => m`
230
+ <svg
231
+ width="256"
232
+ height="256"
233
+ viewBox="0 0 256 256"
234
+ xmlns="http://www.w3.org/2000/svg"
235
+ role="img"
236
+ aria-labelledby="title desc"
237
+ >
238
+ <title id="title">Hearald</title>
239
+ <desc id="desc">
240
+ Circular seal with a centered Gothic-style H monogram.
241
+ </desc>
242
+
243
+ <style>
244
+ .stroke {
245
+ stroke: white;
246
+ stroke-width: 8;
247
+ stroke-linecap: round;
248
+ stroke-linejoin: round;
249
+ fill: none;
250
+ }
251
+ .fill {
252
+ fill: #127d44;
253
+ }
254
+ </style>
255
+
256
+ <!-- Outer seal circle -->
257
+ <circle class="stroke" cx="128" cy="128" r="112" />
258
+
259
+ <!-- Inner decorative circle -->
260
+ <circle class="stroke" cx="128" cy="128" r="96" stroke-dasharray="14 10" />
261
+
262
+ <!-- Gothic-style H (stylized manually) -->
263
+ <g transform="translate(78,72)">
264
+ <!-- Left vertical stroke with spiked top/bottom -->
265
+ <path class="fill" d="M8 0 L20 0 L20 112 L8 112 L0 100 L0 12 Z" />
266
+ <!-- Right vertical stroke with spiked top/bottom -->
267
+ <path class="fill" d="M80 0 L92 0 L100 12 L100 100 L92 112 L80 112 Z" />
268
+ <!-- Crossbar with gothic curve -->
269
+ <path class="fill" d="M20 48 H80 V64 H20 Z" />
270
+ <!-- Decorative inward serifs -->
271
+ <path class="fill" d="M20 0 q10 8 0 16 Z" />
272
+ <path class="fill" d="M80 0 q-10 8 0 16 Z" />
273
+ <path class="fill" d="M20 96 q10 8 0 16 Z" />
274
+ <path class="fill" d="M80 96 q-10 8 0 16 Z" />
275
+ </g>
276
+ </svg>
277
+ `;
278
+
279
+ const getDataFn = ({ n, d, q }) =>
280
+ getData({
281
+ n,
282
+ d,
283
+ q,
284
+ endpointBuilder: (n, d, q) => {
285
+ return `${window.location.pathname.replace(/\/$/, "")}/data/${n}/${d}${
286
+ q ? "?" + q : ""
287
+ }`;
288
+ },
289
+ fetchStats: getJson,
290
+ });
291
+
292
+ let UI = [];
293
+
294
+ async function fetchUI() {
295
+ try {
296
+ const res = await fetch(window.location.href + "/layout");
297
+ UI = await res.json();
298
+ } catch (e) {
299
+ UI = [];
300
+ }
301
+ }
302
+
303
+ class App extends x {
304
+ constructor(props) {
305
+ super(props);
306
+ this.state = {
307
+ numDays: 30,
308
+ date: DATE,
309
+ chartField: "users",
310
+ chartContext: "all",
311
+ uiSectionsLoaded: false,
312
+ };
313
+ }
314
+
315
+ async componentDidMount() {
316
+ await fetchUI();
317
+ this.setState({ uiSectionsLoaded: true });
318
+ fetchData({
319
+ numDays: this.state.numDays,
320
+ date: this.state.date,
321
+ getDataFn,
322
+ onLoadComplete: (stats) => this.setState({ stats }),
323
+ });
324
+ fetchChartData({
325
+ numDays: this.state.numDays,
326
+ date: this.state.date,
327
+ chartContext: this.state.chartContext,
328
+ chartField: this.state.chartField,
329
+ getDataFn,
330
+ onLoadProgress: (i, n) => {
331
+ this.setState({ chartData: { loading: { max: n, value: i } } });
332
+ },
333
+ onLoadComplete: (data, max) => {
334
+ this.setState({ chartData: { loading: false, data, max } });
335
+ },
336
+ });
337
+ }
338
+
339
+ async fetchChartData({ numDays, date, chartContext, chartField }) {
340
+ await fetchChartData({
341
+ numDays: numDays || this.state.numDays,
342
+ date: date || this.state.date,
343
+ chartContext: chartContext || this.state.chartContext,
344
+ chartField: chartField || this.state.chartField,
345
+ getDataFn,
346
+ onLoadProgress: (i, n) =>
347
+ this.setState({ chartData: { loading: { max: n, value: i } } }),
348
+ onLoadComplete: (data, max) =>
349
+ this.setState({ chartData: { loading: false, data, max } }),
350
+ });
351
+ }
352
+
353
+ async fetchData({ numDays, date }) {
354
+ await fetchData({
355
+ numDays: numDays || this.state.numDays,
356
+ date: date || this.state.date,
357
+ getDataFn,
358
+ onLoadComplete: (stats) => this.setState({ stats }),
359
+ });
360
+ }
361
+
362
+ changeNumDays(e) {
363
+ const numDays = e.target.value;
364
+ this.setState({ numDays });
365
+ this.fetchData({ numDays });
366
+ this.fetchChartData({ numDays });
367
+ }
368
+
369
+ changeDate(e) {
370
+ const date = e.target.value;
371
+ this.setState({ date });
372
+ this.fetchData({ date });
373
+ this.fetchChartData({ date });
374
+ }
375
+
376
+ changeChartField(e) {
377
+ const chartField = e.target.value;
378
+ this.setState({ chartField });
379
+ this.fetchChartData({ chartField });
380
+ }
381
+
382
+ changeChartContext(e) {
383
+ const chartContext = e.target.value;
384
+ this.setState({ chartContext });
385
+ this.fetchChartData({ chartContext });
386
+ }
387
+
388
+ render(
389
+ _props,
390
+ {
391
+ chartField,
392
+ chartContext,
393
+ numDays,
394
+ date,
395
+ stats = null,
396
+ chartData = null,
397
+ uiSectionsLoaded,
398
+ }
399
+ ) {
400
+ return m`
401
+ <div id="analytics">
402
+ <div class="analytics__header">
403
+ <div class="header__logo">
404
+ <${Logo} />
405
+ </div>
406
+ <span>Hearald</span>
407
+ <input
408
+ type="text"
409
+ class="analytics__header__input"
410
+ onchange=${(e) => this.changeNumDays(e)}
411
+ value=${numDays}
412
+ />
413
+ <input
414
+ type="text"
415
+ class="analytics__header__input"
416
+ onchange=${(e) => this.changeDate(e)}
417
+ value=${date}
418
+ />
419
+ </div>
420
+ ${!uiSectionsLoaded || !stats || !stats.metrics
421
+ ? m` <div><section class="analytics">Loading...</section></div> `
422
+ : m`
423
+ <div>
424
+ <section class="analytics analytics--chart">
425
+ <form
426
+ class="analytics__menu"
427
+ onsubmit=${(e) => e.preventDefault()}
428
+ >
429
+ <label>
430
+ Context
431
+ <select
432
+ name="context"
433
+ onchange=${(e) => this.changeChartContext(e)}
434
+ >
435
+ ${UI.chartContexts.map(
436
+ ({ id, menuName }) =>
437
+ m`
438
+ <option
439
+ value="${id}"
440
+ selected=${chartContext === id}
441
+ >
442
+ ${menuName}
443
+ </option>
444
+ `
445
+ )}
446
+ </select>
447
+ </label>
448
+ <label>
449
+ Field
450
+ <select
451
+ name="field"
452
+ onchange=${(e) => this.changeChartField(e)}
453
+ >
454
+ ${stats?.metrics?.all
455
+ ? Object.keys(stats.metrics.all).map(
456
+ (f) =>
457
+ m`
458
+ <option
459
+ value="${f}"
460
+ selected=${chartField === f}
461
+ >
462
+ ${f}
463
+ </option>
464
+ `
465
+ )
466
+ : ""}
467
+ </select>
468
+ </label>
469
+ </form>
470
+ <div class="bar-chart">
471
+ ${!chartData &&
472
+ m` <span class="bar-chart__empty">no data</span> `}
473
+ ${chartData &&
474
+ chartData.loading &&
475
+ m`
476
+ <progress
477
+ class="bar-chart__empty"
478
+ max=${chartData.loading.max}
479
+ value=${chartData.loading.value}
480
+ ></progress>
481
+ `}
482
+ ${chartData &&
483
+ !chartData.loading &&
484
+ m`
485
+ <div class="bar-chart__scale">${chartData.max}</div>
486
+ ${chartData.data.map(
487
+ (v) =>
488
+ m`
489
+ <div
490
+ class="bar-chart__bar"
491
+ style="--bar-height:${v.value / chartData.max}"
492
+ >
493
+ <div class="bar-chart__bar__label">
494
+ ${v.date}
495
+ <br />
496
+ <b>${numFormatter(v.value)}</b>
497
+ </div>
498
+ </div>
499
+ `
500
+ )}
501
+ `}
502
+ </div>
503
+ </section>
504
+ ${UI.sections.map((statsSection) => {
505
+ return m`
506
+ <section class="analytics analytics--${statsSection.id}">
507
+ ${statsSection.menuName !== "" &&
508
+ m`
509
+ <h2 class="analytics__title">
510
+ ${statsSection.menuName}
511
+ </h2>
512
+ `}
513
+ ${statsSection.stats.map((f) => {
514
+ return m`
515
+ <${Tile}
516
+ title="${f.label}"
517
+ type="${f.type}"
518
+ value=${deepGet(stats.metrics, f.key)}
519
+ span=${f.span || null}
520
+ ${f.type === "percent" ? "percent" : ""}
521
+ ${f.type === "histogram" ? "histogram" : ""}
522
+ />
523
+ `;
524
+ })}
525
+ </section>
526
+ `;
527
+ })}
528
+ </div>
529
+ `}
530
+ </div>
531
+ `;
532
+ }
533
+ }
534
+
535
+ G(m` <${App} /> `, document.getElementById("analytics-root"));
536
+
537
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hearald",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "A single server drop-in file system based metrics and analytics library.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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
+ }