@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.
Files changed (128) hide show
  1. package/browser/index.js +8 -0
  2. package/browser/report-error.js +105 -0
  3. package/components/bot-scores.marko.js +2 -2
  4. package/components/browser-component/script.marko.js +2 -2
  5. package/components/browser-component.marko.js +2 -2
  6. package/components/deferred-stylesheet.marko.js +2 -2
  7. package/components/document/components/body-wrapper.marko.js +2 -2
  8. package/components/document/components/error.marko.js +2 -2
  9. package/components/document/components/live-reload.marko.js +2 -2
  10. package/components/document/components/polyfill.marko.js +2 -2
  11. package/components/document/components/styles.marko.js +2 -2
  12. package/components/document/container.marko.js +2 -2
  13. package/components/document/index.marko.js +2 -2
  14. package/components/element/array.marko.js +2 -2
  15. package/components/element/audio.marko.js +2 -2
  16. package/components/element/block.marko.js +2 -2
  17. package/components/element/clear.marko.js +2 -2
  18. package/components/element/components/image.marko.js +2 -2
  19. package/components/element/components/text.marko.js +2 -2
  20. package/components/element/content/address1.marko.js +2 -2
  21. package/components/element/content/address2.marko.js +2 -2
  22. package/components/element/content/audio.marko.js +2 -2
  23. package/components/element/content/authors.marko.js +2 -2
  24. package/components/element/content/body.marko.js +2 -2
  25. package/components/element/content/byline.marko.js +2 -2
  26. package/components/element/content/city-state-zip.marko.js +2 -2
  27. package/components/element/content/contributors.marko.js +2 -2
  28. package/components/element/content/country.marko.js +2 -2
  29. package/components/element/content/embed-code.marko.js +2 -2
  30. package/components/element/content/end-date.marko.js +2 -2
  31. package/components/element/content/ends.marko.js +2 -2
  32. package/components/element/content/fax.marko.js +2 -2
  33. package/components/element/content/images.marko.js +2 -2
  34. package/components/element/content/mobile.marko.js +2 -2
  35. package/components/element/content/name.marko.js +2 -2
  36. package/components/element/content/phone.marko.js +2 -2
  37. package/components/element/content/photographers.marko.js +2 -2
  38. package/components/element/content/public-email.marko.js +2 -2
  39. package/components/element/content/published.marko.js +2 -2
  40. package/components/element/content/short-name.marko.js +2 -2
  41. package/components/element/content/sidebar-stubs.marko.js +2 -2
  42. package/components/element/content/sidebars.marko.js +2 -2
  43. package/components/element/content/source.marko.js +2 -2
  44. package/components/element/content/speakers.marko.js +2 -2
  45. package/components/element/content/sponsors.marko.js +2 -2
  46. package/components/element/content/start-date.marko.js +2 -2
  47. package/components/element/content/starts.marko.js +2 -2
  48. package/components/element/content/teaser.marko.js +2 -2
  49. package/components/element/content/title.marko.js +2 -2
  50. package/components/element/content/tollfree.marko.js +2 -2
  51. package/components/element/content/transcript.marko.js +2 -2
  52. package/components/element/content/website-deck.marko.js +2 -2
  53. package/components/element/content/website.marko.js +2 -2
  54. package/components/element/date.marko.js +2 -2
  55. package/components/element/image/caption.marko.js +2 -2
  56. package/components/element/image/credit.marko.js +2 -2
  57. package/components/element/image/display-name.marko.js +2 -2
  58. package/components/element/image/slider.marko.js +2 -2
  59. package/components/element/img.marko.js +2 -2
  60. package/components/element/index.marko.js +2 -2
  61. package/components/element/link.marko.js +2 -2
  62. package/components/element/magazine-issue/description.marko.js +2 -2
  63. package/components/element/magazine-issue/digital-edition-url.marko.js +2 -2
  64. package/components/element/magazine-issue/name.marko.js +2 -2
  65. package/components/element/magazine-issue/pdf-url.marko.js +2 -2
  66. package/components/element/magazine-publication/cancel-url.marko.js +2 -2
  67. package/components/element/magazine-publication/change-address-url.marko.js +2 -2
  68. package/components/element/magazine-publication/description.marko.js +2 -2
  69. package/components/element/magazine-publication/einquiry-url.marko.js +2 -2
  70. package/components/element/magazine-publication/name.marko.js +2 -2
  71. package/components/element/magazine-publication/renewal-url.marko.js +2 -2
  72. package/components/element/magazine-publication/reprints-url.marko.js +2 -2
  73. package/components/element/magazine-publication/subscribe-url.marko.js +2 -2
  74. package/components/element/obj-array.marko.js +2 -2
  75. package/components/element/obj-audio.marko.js +2 -2
  76. package/components/element/obj-date.marko.js +2 -2
  77. package/components/element/obj-nodes.marko.js +2 -2
  78. package/components/element/obj-text.marko.js +2 -2
  79. package/components/element/obj.marko.js +2 -2
  80. package/components/element/picture.marko.js +2 -2
  81. package/components/element/text.marko.js +2 -2
  82. package/components/element/website-section/description.marko.js +2 -2
  83. package/components/element/website-section/hierarchy.marko.js +2 -2
  84. package/components/element/website-section/name.marko.js +2 -2
  85. package/components/font/google.marko.js +2 -2
  86. package/components/font/link.marko.js +2 -2
  87. package/components/font/typekit.marko.js +2 -2
  88. package/components/load-more/index.marko.js +2 -2
  89. package/components/load-more/trigger.marko.js +2 -2
  90. package/components/node/body.marko.js +2 -2
  91. package/components/node/element.marko.js +2 -2
  92. package/components/node/footer.marko.js +2 -2
  93. package/components/node/header.marko.js +2 -2
  94. package/components/node/image-inner-wrapper.marko.js +2 -2
  95. package/components/node/image-wrapper.marko.js +2 -2
  96. package/components/node/image.marko.js +2 -2
  97. package/components/node/index.marko.js +2 -2
  98. package/components/node-list/body.marko.js +2 -2
  99. package/components/node-list/element.marko.js +2 -2
  100. package/components/node-list/footer.marko.js +2 -2
  101. package/components/node-list/header.marko.js +2 -2
  102. package/components/node-list/index.marko.js +2 -2
  103. package/components/node-list/node.marko.js +2 -2
  104. package/components/node-list/nodes.marko.js +2 -2
  105. package/components/page/container.marko.js +2 -2
  106. package/components/page/description.marko.js +2 -2
  107. package/components/page/image.marko.js +2 -2
  108. package/components/page/layouts/content.marko.js +2 -2
  109. package/components/page/layouts/default.marko.js +2 -2
  110. package/components/page/layouts/dynamic-page.marko.js +2 -2
  111. package/components/page/layouts/magazine-issue.marko.js +2 -2
  112. package/components/page/layouts/magazine-publication.marko.js +2 -2
  113. package/components/page/layouts/website-section.marko.js +2 -2
  114. package/components/page/metadata/components/common.marko.js +2 -2
  115. package/components/page/metadata/content.marko.js +2 -2
  116. package/components/page/metadata/default.marko.js +2 -2
  117. package/components/page/metadata/dynamic-page.marko.js +2 -2
  118. package/components/page/metadata/magazine-issue.marko.js +2 -2
  119. package/components/page/metadata/magazine-publication.marko.js +2 -2
  120. package/components/page/metadata/website-section.marko.js +2 -2
  121. package/components/page/rel-canonical.marko.js +2 -2
  122. package/components/page/title.marko.js +2 -2
  123. package/components/page/wrapper.marko.js +2 -2
  124. package/components/resolve/page.marko.js +2 -2
  125. package/components/rss/website-section.marko.js +2 -2
  126. package/express/client-errors.js +114 -0
  127. package/express/index.js +4 -0
  128. 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.91.0",
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.84.0",
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.84.0",
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.83.1",
37
- "@mindful-web/marko-web-deferred-script-loader": "^1.83.1",
38
- "@mindful-web/object-path": "^1.83.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.83.1",
41
- "@mindful-web/web-common": "^1.86.0",
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": "c1e3970f3c20e31a94eb5cd0a910316f4f8c4980"
65
+ "gitHead": "2f871b393fbf9fbf0cefcedd70c7f356cb457d0f"
66
66
  }