@mindful-web/marko-web 1.91.0 → 1.92.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/browser/index.js +8 -0
- package/browser/report-error.js +105 -0
- package/components/bot-scores.marko.js +2 -2
- package/components/browser-component/script.marko.js +2 -2
- package/components/browser-component.marko.js +2 -2
- package/components/deferred-stylesheet.marko.js +2 -2
- package/components/document/components/body-wrapper.marko.js +2 -2
- package/components/document/components/error.marko.js +2 -2
- package/components/document/components/live-reload.marko.js +2 -2
- package/components/document/components/polyfill.marko.js +2 -2
- package/components/document/components/styles.marko.js +2 -2
- package/components/document/container.marko.js +2 -2
- package/components/document/index.marko.js +2 -2
- package/components/element/array.marko.js +2 -2
- package/components/element/audio.marko.js +2 -2
- package/components/element/block.marko.js +2 -2
- package/components/element/clear.marko.js +2 -2
- package/components/element/components/image.marko.js +2 -2
- package/components/element/components/text.marko.js +2 -2
- package/components/element/content/address1.marko.js +2 -2
- package/components/element/content/address2.marko.js +2 -2
- package/components/element/content/audio.marko.js +2 -2
- package/components/element/content/authors.marko.js +2 -2
- package/components/element/content/body.marko.js +2 -2
- package/components/element/content/byline.marko.js +2 -2
- package/components/element/content/city-state-zip.marko.js +2 -2
- package/components/element/content/contributors.marko.js +2 -2
- package/components/element/content/country.marko.js +2 -2
- package/components/element/content/embed-code.marko.js +2 -2
- package/components/element/content/end-date.marko.js +2 -2
- package/components/element/content/ends.marko.js +2 -2
- package/components/element/content/fax.marko.js +2 -2
- package/components/element/content/images.marko.js +2 -2
- package/components/element/content/mobile.marko.js +2 -2
- package/components/element/content/name.marko.js +2 -2
- package/components/element/content/phone.marko.js +2 -2
- package/components/element/content/photographers.marko.js +2 -2
- package/components/element/content/public-email.marko.js +2 -2
- package/components/element/content/published.marko.js +2 -2
- package/components/element/content/short-name.marko.js +2 -2
- package/components/element/content/sidebar-stubs.marko.js +2 -2
- package/components/element/content/sidebars.marko.js +2 -2
- package/components/element/content/source.marko.js +2 -2
- package/components/element/content/speakers.marko.js +2 -2
- package/components/element/content/sponsors.marko.js +2 -2
- package/components/element/content/start-date.marko.js +2 -2
- package/components/element/content/starts.marko.js +2 -2
- package/components/element/content/teaser.marko.js +2 -2
- package/components/element/content/title.marko.js +2 -2
- package/components/element/content/tollfree.marko.js +2 -2
- package/components/element/content/transcript.marko.js +2 -2
- package/components/element/content/website-deck.marko.js +2 -2
- package/components/element/content/website.marko.js +2 -2
- package/components/element/date.marko.js +2 -2
- package/components/element/image/caption.marko.js +2 -2
- package/components/element/image/credit.marko.js +2 -2
- package/components/element/image/display-name.marko.js +2 -2
- package/components/element/image/slider.marko.js +2 -2
- package/components/element/img.marko.js +2 -2
- package/components/element/index.marko.js +2 -2
- package/components/element/link.marko.js +2 -2
- package/components/element/magazine-issue/description.marko.js +2 -2
- package/components/element/magazine-issue/digital-edition-url.marko.js +2 -2
- package/components/element/magazine-issue/name.marko.js +2 -2
- package/components/element/magazine-issue/pdf-url.marko.js +2 -2
- package/components/element/magazine-publication/cancel-url.marko.js +2 -2
- package/components/element/magazine-publication/change-address-url.marko.js +2 -2
- package/components/element/magazine-publication/description.marko.js +2 -2
- package/components/element/magazine-publication/einquiry-url.marko.js +2 -2
- package/components/element/magazine-publication/name.marko.js +2 -2
- package/components/element/magazine-publication/renewal-url.marko.js +2 -2
- package/components/element/magazine-publication/reprints-url.marko.js +2 -2
- package/components/element/magazine-publication/subscribe-url.marko.js +2 -2
- package/components/element/obj-array.marko.js +2 -2
- package/components/element/obj-audio.marko.js +2 -2
- package/components/element/obj-date.marko.js +2 -2
- package/components/element/obj-nodes.marko.js +2 -2
- package/components/element/obj-text.marko.js +2 -2
- package/components/element/obj.marko.js +2 -2
- package/components/element/picture.marko.js +2 -2
- package/components/element/text.marko.js +2 -2
- package/components/element/website-section/description.marko.js +2 -2
- package/components/element/website-section/hierarchy.marko.js +2 -2
- package/components/element/website-section/name.marko.js +2 -2
- package/components/font/google.marko.js +2 -2
- package/components/font/link.marko.js +2 -2
- package/components/font/typekit.marko.js +2 -2
- package/components/load-more/index.marko.js +2 -2
- package/components/load-more/trigger.marko.js +2 -2
- package/components/node/body.marko.js +2 -2
- package/components/node/element.marko.js +2 -2
- package/components/node/footer.marko.js +2 -2
- package/components/node/header.marko.js +2 -2
- package/components/node/image-inner-wrapper.marko.js +2 -2
- package/components/node/image-wrapper.marko.js +2 -2
- package/components/node/image.marko.js +2 -2
- package/components/node/index.marko.js +2 -2
- package/components/node-list/body.marko.js +2 -2
- package/components/node-list/element.marko.js +2 -2
- package/components/node-list/footer.marko.js +2 -2
- package/components/node-list/header.marko.js +2 -2
- package/components/node-list/index.marko.js +2 -2
- package/components/node-list/node.marko.js +2 -2
- package/components/node-list/nodes.marko.js +2 -2
- package/components/page/container.marko.js +2 -2
- package/components/page/description.marko.js +2 -2
- package/components/page/image.marko.js +2 -2
- package/components/page/layouts/content.marko.js +2 -2
- package/components/page/layouts/default.marko.js +2 -2
- package/components/page/layouts/dynamic-page.marko.js +2 -2
- package/components/page/layouts/magazine-issue.marko.js +2 -2
- package/components/page/layouts/magazine-publication.marko.js +2 -2
- package/components/page/layouts/website-section.marko.js +2 -2
- package/components/page/metadata/components/common.marko.js +2 -2
- package/components/page/metadata/content.marko.js +2 -2
- package/components/page/metadata/default.marko.js +2 -2
- package/components/page/metadata/dynamic-page.marko.js +2 -2
- package/components/page/metadata/magazine-issue.marko.js +2 -2
- package/components/page/metadata/magazine-publication.marko.js +2 -2
- package/components/page/metadata/website-section.marko.js +2 -2
- package/components/page/rel-canonical.marko.js +2 -2
- package/components/page/title.marko.js +2 -2
- package/components/page/wrapper.marko.js +2 -2
- package/components/resolve/page.marko.js +2 -2
- package/components/rss/website-section.marko.js +2 -2
- package/express/client-errors.js +114 -0
- package/express/index.js +4 -0
- package/package.json +9 -9
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const { newrelicAgent } = require('@mindful-web/utils');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Receives client-side error reports and records them as New Relic `ClientError`
|
|
6
|
+
* custom events.
|
|
7
|
+
*
|
|
8
|
+
* Browser failures are otherwise invisible: there is no New Relic Browser agent on the
|
|
9
|
+
* fleet, and a `fetch` that is challenged or blocked at the edge never reaches the
|
|
10
|
+
* origin, so nothing appears in server-side telemetry either. This route is the seam
|
|
11
|
+
* that makes those failures countable.
|
|
12
|
+
*
|
|
13
|
+
* Two rules govern everything here:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Never fail.** The response is always 204, whatever the body contains and whether
|
|
16
|
+
* or not a New Relic agent exists. A reporting endpoint that can error is a reporting
|
|
17
|
+
* endpoint that generates the errors it is meant to measure.
|
|
18
|
+
* 2. **Never trust the body.** Identity (host/tenant/site) is supplied by the server, not
|
|
19
|
+
* by the payload, so a report cannot attribute itself to another site.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const MAX_MESSAGE = 500;
|
|
23
|
+
const MAX_STACK = 1000;
|
|
24
|
+
const MAX_URL = 1000;
|
|
25
|
+
const MAX_SOURCE = 100;
|
|
26
|
+
|
|
27
|
+
// Ceiling on events recorded per process per minute. The browser helper already
|
|
28
|
+
// de-duplicates and caps per page view; this is the backstop for the case that cap cannot
|
|
29
|
+
// cover — many clients failing at once, or a forged payload. A single global counter is
|
|
30
|
+
// deliberate: per-IP state would grow unbounded and store client addresses to no benefit,
|
|
31
|
+
// and the question this data answers ("how often does this happen") survives a ceiling.
|
|
32
|
+
const MAX_EVENTS_PER_MINUTE = 600;
|
|
33
|
+
|
|
34
|
+
let windowStart = 0;
|
|
35
|
+
let windowCount = 0;
|
|
36
|
+
|
|
37
|
+
const withinRateLimit = () => {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
if (now - windowStart >= 60000) {
|
|
40
|
+
windowStart = now;
|
|
41
|
+
windowCount = 0;
|
|
42
|
+
}
|
|
43
|
+
windowCount += 1;
|
|
44
|
+
return windowCount <= MAX_EVENTS_PER_MINUTE;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* New Relic drops nested values, so every attribute must be a primitive. Non-strings are
|
|
49
|
+
* discarded rather than coerced — `[object Object]` rows are worse than absent ones.
|
|
50
|
+
*/
|
|
51
|
+
const str = (value, max) => {
|
|
52
|
+
if (typeof value !== 'string' || !value) return undefined;
|
|
53
|
+
return value.slice(0, max);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const int = (value) => (Number.isFinite(value) ? Math.trunc(value) : undefined);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {import('express').Express} app
|
|
60
|
+
* @param {object} params
|
|
61
|
+
* @param {string} [params.tenantKey]
|
|
62
|
+
* @param {string} [params.siteId]
|
|
63
|
+
* @param {string} [params.mountPoint]
|
|
64
|
+
*/
|
|
65
|
+
module.exports = (app, { tenantKey, siteId, mountPoint = '/__client-error' } = {}) => {
|
|
66
|
+
const parseJson = express.json({ limit: '8kb' });
|
|
67
|
+
|
|
68
|
+
app.post(mountPoint, (req, res) => {
|
|
69
|
+
// The parser is invoked directly rather than mounted as middleware so that its own
|
|
70
|
+
// failures stay here. Mounted, a malformed body yields a 400 and an oversized one a
|
|
71
|
+
// 413, both thrown onward into the app's error handler — which would have this route
|
|
72
|
+
// manufacturing the very errors it exists to measure.
|
|
73
|
+
parseJson(req, res, (parseError) => {
|
|
74
|
+
// Answer before doing any work: the client is already in a failure state and must
|
|
75
|
+
// never be made to wait on telemetry.
|
|
76
|
+
res.status(204).end();
|
|
77
|
+
if (parseError) return;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
if (!withinRateLimit()) return;
|
|
81
|
+
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
82
|
+
const { config } = app.locals;
|
|
83
|
+
|
|
84
|
+
newrelicAgent.recordCustomEvent('ClientError', {
|
|
85
|
+
message: str(body.message, MAX_MESSAGE) || 'Unknown client error',
|
|
86
|
+
name: str(body.name, 100),
|
|
87
|
+
source: str(body.source, MAX_SOURCE) || 'unknown',
|
|
88
|
+
componentName: str(body.componentName, 100),
|
|
89
|
+
stack: str(body.stack, MAX_STACK),
|
|
90
|
+
pageUrl: str(body.pageUrl, MAX_URL),
|
|
91
|
+
// Distinguishes an edge challenge (403 + HTML) from a genuine application error.
|
|
92
|
+
statusCode: int(body.statusCode),
|
|
93
|
+
cfRay: str(body.cfRay, 50),
|
|
94
|
+
cfMitigated: str(body.cfMitigated, 50),
|
|
95
|
+
// Caller-supplied context (e.g. the leaders section that failed).
|
|
96
|
+
sectionId: str(body.sectionId, 100),
|
|
97
|
+
sectionName: str(body.sectionName, 200),
|
|
98
|
+
// Set by the global `window.onerror` handler.
|
|
99
|
+
pageSource: str(body.pageSource, MAX_URL),
|
|
100
|
+
lineNumber: int(body.lineNumber),
|
|
101
|
+
// Identity comes from the server, never the payload. Named `siteHost` because
|
|
102
|
+
// New Relic reserves `host` for the reporting machine and silently overwrites
|
|
103
|
+
// it — the first production week recorded 211 pod names and zero site hosts.
|
|
104
|
+
siteHost: config ? config.website('host') : undefined,
|
|
105
|
+
tenantKey,
|
|
106
|
+
siteId,
|
|
107
|
+
userAgent: str(req.get('user-agent'), 500),
|
|
108
|
+
});
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// Telemetry must never break a request — the response has already been sent.
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
};
|
package/express/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const helmet = require('helmet');
|
|
7
7
|
const { nanoid } = require('nanoid');
|
|
8
8
|
const apollo = require('./apollo');
|
|
9
|
+
const clientErrors = require('./client-errors');
|
|
9
10
|
const graphqlProxy = require('./graphql-proxy');
|
|
10
11
|
const embeddedMedia = require('./embedded-media');
|
|
11
12
|
const loadObject = require('./load-object');
|
|
@@ -110,6 +111,9 @@ module.exports = (config = {}) => {
|
|
|
110
111
|
});
|
|
111
112
|
graphqlProxy(app, { graphqlUri, headers });
|
|
112
113
|
|
|
114
|
+
// Register the client-side error beacon.
|
|
115
|
+
clientErrors(app, { tenantKey, siteId });
|
|
116
|
+
|
|
113
117
|
// Set website context.
|
|
114
118
|
app.use(websiteContext(app.locals.config));
|
|
115
119
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindful-web/marko-web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.92.1",
|
|
4
4
|
"description": "Core Marko+Express components for Mindful Web websites",
|
|
5
5
|
"author": "Jacob Bare <jacob@parameter1.com>",
|
|
6
6
|
"main": "index.js",
|
|
@@ -26,19 +26,19 @@
|
|
|
26
26
|
"@godaddy/terminus": "^4.11.2",
|
|
27
27
|
"@mindful-web/apollo-ssr": "^1.0.0",
|
|
28
28
|
"@mindful-web/dayjs": "^1.49.2",
|
|
29
|
-
"@mindful-web/embedded-media": "^1.
|
|
29
|
+
"@mindful-web/embedded-media": "^1.92.0",
|
|
30
30
|
"@mindful-web/express-apollo": "^1.0.0",
|
|
31
31
|
"@mindful-web/graphql-fragment-types": "^1.0.0",
|
|
32
32
|
"@mindful-web/html": "^1.84.0",
|
|
33
|
-
"@mindful-web/image": "^1.
|
|
33
|
+
"@mindful-web/image": "^1.92.0",
|
|
34
34
|
"@mindful-web/inflector": "^1.0.0",
|
|
35
35
|
"@mindful-web/marko-express": "^1.70.3",
|
|
36
|
-
"@mindful-web/marko-node-require": "^1.
|
|
37
|
-
"@mindful-web/marko-web-deferred-script-loader": "^1.
|
|
38
|
-
"@mindful-web/object-path": "^1.
|
|
36
|
+
"@mindful-web/marko-node-require": "^1.92.0",
|
|
37
|
+
"@mindful-web/marko-web-deferred-script-loader": "^1.92.0",
|
|
38
|
+
"@mindful-web/object-path": "^1.92.0",
|
|
39
39
|
"@mindful-web/tenant-context": "^1.0.0",
|
|
40
|
-
"@mindful-web/utils": "^1.
|
|
41
|
-
"@mindful-web/web-common": "^1.
|
|
40
|
+
"@mindful-web/utils": "^1.92.0",
|
|
41
|
+
"@mindful-web/web-common": "^1.92.0",
|
|
42
42
|
"async": "^3.2.4",
|
|
43
43
|
"cheerio": "^1.0.0-rc.12",
|
|
44
44
|
"cookie-parser": "^1.4.6",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"publishConfig": {
|
|
63
63
|
"access": "public"
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "2f871b393fbf9fbf0cefcedd70c7f356cb457d0f"
|
|
66
66
|
}
|