brut-js 0.0.10 → 0.0.20

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.
@@ -36,7 +36,7 @@ class ConstraintViolationMessages extends BaseCustomElement {
36
36
  * This should be called as part of a Form validation event to provide a customized UX for
37
37
  * the error messages, beyond what the browser would do by default. The keys used are the same
38
38
  * as the attributes of a `ValidityState`, so for example, a range underflow would mean that `validity.rangeUnderflow` would return
39
- * true. Thus, a `<brut-cv>` would be created with `key="general.cv.fe.rangeUnderflow"`.
39
+ * true. Thus, a `<brut-cv>` would be created with `key="cv.fe.rangeUnderflow"`.
40
40
  *
41
41
  * The `cv.fe` is hard-coded to be consistent with Brut's server-side translation management.
42
42
  *
package/src/Tracing.js ADDED
@@ -0,0 +1,245 @@
1
+ import { BaseCustomElement } from "brut-js"
2
+
3
+ /** Sends performance data to an endpoint in a Brut-powered app that is expected to save it as an Open Telemetry span.
4
+ * Uses the W3C-recommended headers "traceparent" and "tracestate" to do this.
5
+ *
6
+ * ### Supported Metrics
7
+ *
8
+ * Currently, this will attempt to send "navigation", "largest-contentful-paint", and "first-contentful-paint" back to the server.
9
+ * Not all browsers support these, so this element will send back as many as it can. It will wait for all supported metrics to be
10
+ * received before contacting the server. It will attempt to do this exactly once.
11
+ *
12
+ * ### Use
13
+ *
14
+ * To use this element, your page must have a `<meta>` element that contains the value for "traceparent". It is expected that your
15
+ * server will include this in server-generatd HTML. The Brut's `Brut::FrontEnd::Components::Traceparent` component will handle this
16
+ * for you. The value for "traceparent" is key to connecting the browser metrics to the back-end request that generated the page.
17
+ *
18
+ * The element also requires a `url` attribute to know where to send the data. By default, Brut is listening in
19
+ * `/__brut/instrumentation`. See the example.
20
+ *
21
+ * ### Durations vs Timestamps
22
+ *
23
+ * The performance API produces durations since an origin timestamp. Open Telemetry wants timestamps. In theory,
24
+ * `Performance.timeOrigin` is provided by the browser as a reference time when the page started doing anything.
25
+ * In practice, this value is incorrect on Firefox, so the element records a timestamp when it is created.
26
+ *
27
+ * When the data is merged back to the server span, the specific timestamps will not exactly match reality, however the durations will
28
+ * be accurate. Note that even if `Performance.timeOrigin` was correct, clock drift between client and server would make
29
+ * the timestamps inaccurate anyway.
30
+ *
31
+ * ### Encoding
32
+ *
33
+ * The spec for the "tracestate" header leaves open how the data is to be encoded. It supports multiple vendors using a key/value
34
+ * pair:
35
+ *
36
+ * tracestate: honeycomb=«encoded data»,newrelic=«encoded data»
37
+ *
38
+ * This element uses the vendor name "brut". The data is a Base64-encoded JSON blob containing the data.
39
+ *
40
+ * tracestate: brut=«Base64 encoded JSON»
41
+ *
42
+ * The values captured and format of the JSON map closely to Open Telemetry's browser instrumentation format.
43
+ * Of course, this element is many magnitudes smaller in size than Open Telemetry's, which is why it exists at all
44
+ *
45
+ * @example
46
+ * <!DOCTYPE html>
47
+ * <html>
48
+ * <head>
49
+ * <meta name="traceparent" content="293874293749237439843294">
50
+ * <brut-tracing url="/__brut/instrumentation"></brut-tracing>
51
+ * <!-- ... -->
52
+ * </head>
53
+ * <body>
54
+ * <!-- ... -->
55
+ * </body>
56
+ * </html>
57
+ *
58
+ * @property {string} url - the url where the trace information is to be sent.
59
+ *
60
+ * @see {@link https://www.w3.org/TR/trace-context/}
61
+ * @see external:Performance
62
+ */
63
+ class Tracing extends BaseCustomElement {
64
+ static tagName = "brut-tracing"
65
+
66
+ static observedAttributes = [
67
+ "url",
68
+ "show-warnings",
69
+ ]
70
+
71
+
72
+ #url = null
73
+ #sent = {}
74
+ #payload = {}
75
+ #timeOrigin = null
76
+ #supportedTypes = []
77
+
78
+ #performanceObserver = new PerformanceObserver( (entries) => {
79
+ const navigation = entries.getEntriesByType("navigation")[0]
80
+ if (navigation && navigation.loadEventEnd != 0 && !this.#payload.navigation) {
81
+ this.#payload.navigation = this.#parseNavigation(navigation)
82
+ }
83
+ const largestContentfulPaint = entries.getEntriesByType("largest-contentful-paint")
84
+ if (largestContentfulPaint.length > 0 && !this.#payload["largest-contentful-paint"]) {
85
+ this.#payload["largest-contentful-paint"] = this.#parseLargestContentfulPaint(largestContentfulPaint)
86
+ }
87
+ const paint = entries.getEntriesByName("first-contentful-paint", "paint")[0]
88
+ if (paint && !this.#payload.paint) {
89
+ this.#payload.paint = this.#parseFirstContentfulPaint(paint)
90
+ }
91
+
92
+ if ( this.#supportedTypes.every( (type) => this.#payload[type] ) ) {
93
+ this.#sendSpans()
94
+ this.#payload = {}
95
+ }
96
+ })
97
+
98
+ constructor() {
99
+ super()
100
+ this.#timeOrigin = Date.now()
101
+ this.#supportedTypes = [
102
+ "navigation",
103
+ "largest-contentful-paint",
104
+ "paint",
105
+ ].filter( (type) => {
106
+ return PerformanceObserver.supportedEntryTypes.includes(type)
107
+ })
108
+ }
109
+
110
+ urlChangedCallback({newValue}) {
111
+ this.#url = newValue
112
+ }
113
+
114
+ update() {
115
+ this.#supportedTypes.forEach( (type) => {
116
+ this.#performanceObserver.observe({type: type, buffered: true})
117
+ })
118
+ }
119
+
120
+ #sendSpans() {
121
+ const headers = this.#initializerHeadersIfCanContinue()
122
+ if (!headers) {
123
+ return
124
+ }
125
+ const span = this.#payload.navigation
126
+
127
+ if (this.#payload.paint) {
128
+ span.events.push({
129
+ name: this.#payload.paint.name,
130
+ timestamp: this.#timeOrigin + this.#payload.paint.startTime
131
+ })
132
+ }
133
+ if (this.#payload["largest-contentful-paint"]) {
134
+ this.#payload["largest-contentful-paint"].forEach( (event) => {
135
+ span.events.push({
136
+ name: event.name,
137
+ timestamp: this.#timeOrigin + event.startTime,
138
+ attributes: {
139
+ "element.tag": event.element?.tagName,
140
+ "element.class": event.element?.className,
141
+ }
142
+ })
143
+ })
144
+ }
145
+
146
+ this.#sent[this.#url] = true
147
+ headers.append("tracestate",`brut=${window.btoa(JSON.stringify(span))}`)
148
+ const request = new Request(
149
+ this.#url,
150
+ {
151
+ headers: headers,
152
+ method: "GET",
153
+ }
154
+ )
155
+ fetch(request).then( (response) => {
156
+ if (!response.ok) {
157
+ console.warn("Problem sending instrumentation: %s/%s", response.status,response.statusText)
158
+ }
159
+ }).catch( (error) => {
160
+ console.warn("Problem sending instrumentation: %o", error)
161
+ })
162
+ }
163
+
164
+ #parseNavigation(navigation) {
165
+ const documentFetch = {
166
+ name: "browser.documentFetch",
167
+ start_timestamp: navigation.fetchStart + this.#timeOrigin,
168
+ end_timestamp: navigation.responseEnd + this.#timeOrigin,
169
+ attributes: {
170
+ "http.url": navigation.name,
171
+ },
172
+ }
173
+ const events = [
174
+ "fetchStart",
175
+ "unloadEventStart",
176
+ "unloadEventEnd",
177
+ "domInteractive",
178
+ "domInteractive",
179
+ "domContentLoadedEventStart",
180
+ "domContentLoadedEventEnd",
181
+ "domComplete",
182
+ "loadEventStart",
183
+ "loadEventEnd",
184
+ ]
185
+
186
+ return {
187
+ name: "browser.documentLoad",
188
+ start_timestamp: navigation.fetchStart + this.#timeOrigin,
189
+ end_timestamp: navigation.loadEventEnd + this.#timeOrigin,
190
+ attributes: {
191
+ "http.url": navigation.name,
192
+ "http.user_agent": window.navigator.userAgent,
193
+ },
194
+ events: events.map( (eventName) => {
195
+ return {
196
+ name: eventName,
197
+ timestamp: this.#timeOrigin + navigation[eventName],
198
+ }
199
+ }),
200
+ spans: [
201
+ documentFetch
202
+ ]
203
+ }
204
+ }
205
+
206
+ #parseFirstContentfulPaint(paint) {
207
+ return {
208
+ name: "browser.first-contentful-paint",
209
+ startTime: paint.startTime,
210
+ }
211
+ }
212
+
213
+ #parseLargestContentfulPaint(largestContentfulPaint) {
214
+ return largestContentfulPaint.map( (entry) => {
215
+ return {
216
+ name: "browser.largest-contentful-paint",
217
+ startTime: entry.startTime,
218
+ element: entry.element,
219
+ }
220
+ })
221
+ }
222
+
223
+ #initializerHeadersIfCanContinue() {
224
+ if (!this.#url) {
225
+ this.logger.info("No url set, no traces will be reported")
226
+ return
227
+ }
228
+ const $traceparent = document.querySelector("meta[name='traceparent']")
229
+ if (!$traceparent) {
230
+ this.logger.info("No <meta name='traceparent' ...> in the document, no traces can be reported")
231
+ return
232
+ }
233
+ if (this.#sent[this.#url]) {
234
+ this.logger.info("Already sent to %s", this.#url)
235
+ return
236
+ }
237
+ const traceparent = $traceparent.getAttribute("content")
238
+ if (!traceparent) {
239
+ this.logger.info("%o had no value for the content attribute, no traces can be reported",$traceparent)
240
+ return
241
+ }
242
+ return new Headers({ traceparent })
243
+ }
244
+ }
245
+ export default Tracing
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import BaseCustomElement from "./BaseCustomElement"
2
- import RichString from "./RichString"
3
2
  import AjaxSubmit from "./AjaxSubmit"
3
+ import Autosubmit from "./Autosubmit"
4
4
  import ConfirmSubmit from "./ConfirmSubmit"
5
5
  import ConfirmationDialog from "./ConfirmationDialog"
6
6
  import ConstraintViolationMessage from "./ConstraintViolationMessage"
@@ -8,10 +8,11 @@ import ConstraintViolationMessages from "./ConstraintViolationMessages"
8
8
  import CopyToClipboard from "./CopyToClipboard"
9
9
  import Form from "./Form"
10
10
  import I18nTranslation from "./I18nTranslation"
11
+ import LocaleDetection from "./LocaleDetection"
11
12
  import Message from "./Message"
13
+ import RichString from "./RichString"
12
14
  import Tabs from "./Tabs"
13
- import LocaleDetection from "./LocaleDetection"
14
- import Autosubmit from "./Autosubmit"
15
+ import Tracing from "./Tracing"
15
16
 
16
17
  /**
17
18
  * This is the code for a test case. It may return a {@link external:Promise} if there is async behavior that must
@@ -39,6 +40,11 @@ import Autosubmit from "./Autosubmit"
39
40
  * })
40
41
  */
41
42
 
43
+ /**
44
+ * @external Performance
45
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Performance|Performance API}
46
+ */
47
+
42
48
  /**
43
49
  * @external Promise
44
50
  * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise|Promise}
@@ -101,6 +107,7 @@ BrutCustomElements.addElementClasses(
101
107
  Tabs,
102
108
  LocaleDetection,
103
109
  Autosubmit,
110
+ Tracing,
104
111
  )
105
112
 
106
113
  export {
@@ -117,5 +124,7 @@ export {
117
124
  LocaleDetection,
118
125
  Message,
119
126
  RichString,
120
- Tabs
127
+ Tabs,
128
+ Tracing
121
129
  }
130
+