brut-js 0.0.9 → 0.0.11
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/package.json +1 -1
- package/specs/Form.spec.js +70 -1
- package/specs/public/js/bundle.js +264 -87
- package/specs/public/js/bundle.js.map +4 -4
- package/src/BaseCustomElement.js +1 -1
- package/src/Form.js +40 -5
- package/src/Tracing.js +245 -0
- package/src/index.js +12 -4
package/src/Form.js
CHANGED
|
@@ -6,11 +6,19 @@ import ConstraintViolationMessages from "./ConstraintViolationMessages"
|
|
|
6
6
|
/** A web component that enhances a form it contains to make constraint validations
|
|
7
7
|
* easier to manage and control.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* (
|
|
13
|
-
*
|
|
9
|
+
* This provides two main features:
|
|
10
|
+
*
|
|
11
|
+
* * While the `:user-invalid` selector allows you to target inputs that have been interacted
|
|
12
|
+
* with (thus avoiding issues when using `:invalid`), this still creates the experience of a
|
|
13
|
+
* user tabbing off of a control and getting an error message. If, instead, you only
|
|
14
|
+
* want to show these errors when a submit has been attempted, this element will
|
|
15
|
+
* set `submitted-invalid` on itself when that happens, thus allowing you to target invalid
|
|
16
|
+
* fields only after a submission attempt.
|
|
17
|
+
* * You may wish to control the messaging of client-side constraint violations
|
|
18
|
+
* beyond what the browser gives you. Assuming your `INPUT` tags are inside a container
|
|
19
|
+
* like `LABEL`, a `brut-cv` tag found in that container
|
|
20
|
+
* (i.e. a sibling of your `INPUT`) will be modified to contain error messages specific
|
|
21
|
+
* to the {@link external:ValidityState} of the control.
|
|
14
22
|
*
|
|
15
23
|
* @fires brut:invalid Fired when any element is found to be invalid
|
|
16
24
|
* @fires brut:valid Fired when no element is found to be invalid. This should be reliable to know
|
|
@@ -29,17 +37,41 @@ import ConstraintViolationMessages from "./ConstraintViolationMessages"
|
|
|
29
37
|
* <brut-cv-messages>
|
|
30
38
|
* </brut-cv-messages>
|
|
31
39
|
* </div>
|
|
40
|
+
* <button>Submit</button>
|
|
41
|
+
* </form>
|
|
42
|
+
* </brut-form>
|
|
43
|
+
* <!-- after a submit of this form, the HTML will effectively be as follows -->
|
|
44
|
+
* <brut-form submitted-invalid>
|
|
45
|
+
* <form ...>
|
|
46
|
+
* <label>
|
|
47
|
+
* <input type="text" required name="username">
|
|
48
|
+
* <brut-cv-messages>
|
|
49
|
+
* <brut-cv>This field is required</brut-cv>
|
|
50
|
+
* </brut-cv-messages>
|
|
51
|
+
* </label>
|
|
52
|
+
* <div> <!-- container need not be a label -->
|
|
53
|
+
* <input type="text" required minlength="4" name="alias">
|
|
54
|
+
* <brut-cv-messages>
|
|
55
|
+
* <brut-cv>This field is required</brut-cv>
|
|
56
|
+
* </brut-cv-messages>
|
|
57
|
+
* </div>
|
|
58
|
+
* <button>Submit</button>
|
|
32
59
|
* </form>
|
|
33
60
|
* </brut-form>
|
|
34
61
|
*
|
|
62
|
+
* @property {boolean} submitted-invalid - set by this element when the form is submitted. Does not trigger any behavior and can be used in CSS.
|
|
35
63
|
* @see ConstraintViolationMessages
|
|
36
64
|
*/
|
|
37
65
|
class Form extends BaseCustomElement {
|
|
38
66
|
static tagName = "brut-form"
|
|
39
67
|
static observedAttributes = [
|
|
68
|
+
"submitted-invalid",
|
|
40
69
|
"show-warnings",
|
|
41
70
|
]
|
|
42
71
|
|
|
72
|
+
#markFormSubmittedInvalid = (event) => {
|
|
73
|
+
this.setAttribute("submitted-invalid","")
|
|
74
|
+
}
|
|
43
75
|
#updateValidity = (event) => {
|
|
44
76
|
this.#updateErrorMessages(event)
|
|
45
77
|
}
|
|
@@ -50,6 +82,8 @@ class Form extends BaseCustomElement {
|
|
|
50
82
|
this.dispatchEvent(new CustomEvent("brut:invalid"))
|
|
51
83
|
}
|
|
52
84
|
|
|
85
|
+
submittedInvalidChangedCallback() {}
|
|
86
|
+
|
|
53
87
|
update() {
|
|
54
88
|
const forms = this.querySelectorAll("form")
|
|
55
89
|
if (forms.length == 0) {
|
|
@@ -59,6 +93,7 @@ class Form extends BaseCustomElement {
|
|
|
59
93
|
forms.forEach( (form) => {
|
|
60
94
|
Array.from(form.elements).forEach( (formElement) => {
|
|
61
95
|
formElement.addEventListener("invalid", this.#updateValidity)
|
|
96
|
+
formElement.addEventListener("invalid", this.#markFormSubmittedInvalid)
|
|
62
97
|
formElement.addEventListener("input", this.#updateValidity)
|
|
63
98
|
})
|
|
64
99
|
form.querySelectorAll(AjaxSubmit.tagName).forEach( (ajaxSubmits) => {
|
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
|
|
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,6 @@ export {
|
|
|
117
124
|
LocaleDetection,
|
|
118
125
|
Message,
|
|
119
126
|
RichString,
|
|
120
|
-
Tabs
|
|
127
|
+
Tabs,
|
|
128
|
+
Tracing
|
|
121
129
|
}
|