Events
Event is the umbrella term for two things. A span is an operation with a duration, like a page load or a fetch call. A span event is something that happened at a single moment inside a span.
This page lists the span types a browser client sends. For the payload they go in, see the traces payload and the errors payload. For the types a PHP client sends, see Events in the PHP protocol docs.
You set the type with the flare.span_type attribute on the span. Flare removes that attribute before it shows the span, so it never appears in the interface.
Page spans
browser_pageload and browser_navigation are container spans. Every other browser span points back at one of them through its parentSpanId chain.
The two carry the same attributes. A browser_pageload is the browser loading a document. A browser_navigation is your router swapping the view without loading a new document.
browser_pageload
The initial page load.
Backdate the start of this span to the browser's navigation start rather than to the moment your script ran. The Navigation Timing entry gives you that. Otherwise the span misses everything that happened before your bundle loaded, which is usually the slow part.
Since it is the root of the trace, it carries the entry point attributes. For a browser page that means flare.entry_point.type is web, flare.entry_point.handler.type is browser, flare.entry_point.value is the full URL, and flare.entry_point.handler.identifier is the route. On top of those:
| Attribute | Type | Description |
|---|---|---|
http.route |
string | The route, the same value as flare.entry_point.handler.identifier |
flare.route.source |
string | route if a router gave you the pattern, url if you fell back to the path |
url.full |
string | Full page URL, redacted |
user_agent.original |
string | User agent string |
http.request.referrer |
string | Referrer URL, redacted |
document.ready_state |
string | Document ready state when the span opened |
It can also carry browser.web_vital.* attributes for any vital that was final before the span closed. See Web vitals.
Flare groups page performance on http.route. Leave it out and the trace still works, but the page will not show up in the Browser Pages numbers.
flare.entry_point.handler.identifier is what Flare shows as the label of the span in a trace.
Getting the route right
The route should be the route pattern, not the URL the user landed on. Send /products/{id}, not /products/9f2c. If you send the concrete path, every product page becomes its own aggregation and the numbers are useless.
Set flare.route.source to route when a router gave you the pattern and url when you had to fall back to location.pathname. Flare shows this so people know whether they are looking at real route grouping or raw paths.
Routers often know where they are going before the address bar changes. If you open the span against that destination and the URL then changes, because of a redirect or because a newer navigation replaced it, update url.full and flare.entry_point.value on the open span. Leave the two route attributes alone. Deriving them again from the new URL turns your pattern back into a concrete path.
browser_navigation
A client-side route change. Same attributes as browser_pageload.
Vitals never belong here. They measure the document, and a client-side route change does not load a new one.
Request spans
browser_fetch
A request made with fetch.
| Attribute | Type | Description |
|---|---|---|
url.full |
string | Request URL, redacted |
http.request.method |
string | HTTP method |
server.address |
string | Request host, when the URL resolved against the page origin |
http.response.status_code |
int | Response status code |
Flare needs both url.full and http.request.method to group the request. It reads the host and path out of url.full, so sending server.address on its own is not enough.
browser_xhr
A request made with XMLHttpRequest. Same attributes as browser_fetch.
Keep the two types separate even though they end up in the same aggregation. Most application code uses fetch these days, so an XMLHttpRequest usually points at a library rather than at something you wrote.
Component spans
browser_component
A component mount.
| Attribute | Type | Description |
|---|---|---|
flare.component.name |
string | Component name. Flare uses the span name when this is missing |
Three rules make a component tree come out right.
Point at the nearest profiled ancestor. Set parentSpanId to the span of the closest profiled component above this one, and to the container when there is none. Spans travel as a flat list, so parentSpanId is the only record of which component sat inside which. Point them all at the container and the tree flattens to one level. The lifecycle page shows a flat list and the tree Flare builds from it side by side.
Skip the components you are not profiling. Profiling is usually opt-in per component, so the nearest profiled ancestor is often not the direct parent in your component tree. An unprofiled component in between is passed over, and its children attach to the closest profiled component above it.
Reserve the parent's id early. Components finish mounting from the inside out, so a child's span normally ends before its parent's. You therefore need the parent's spanId before the parent's span exists. Generate that id when the component starts mounting, not when it finishes.
A child may then appear earlier in the spans array than its parent. That is fine. Flare reads the whole payload before it resolves parents, so the order inside the array carries no meaning.
Flare works out which framework a component belongs to from the flare.framework.name resource attribute, not from the span. That value is lowercase, for example react, vue, svelte or sveltekit.
Web vital spans
browser_web_vital
A zero duration span carrying the vitals that were not ready in time to be added to the page span. Its timestamps and its parent reference both behave differently from every other span, so it has its own page: Web vitals.
Span event types
A browser client sends one span event type. Everything else in a browser trace is a span.
php_glow
A short note the developer left behind for debugging context. It goes in the events array of an error report, not in a trace.
| Attribute | Type | Description |
|---|---|---|
glow.name |
string | Glow name |
glow.level |
string | Glow level |
glow.context |
object | Glow context data |
The php_ prefix is a leftover. The browser client sends the same type string the PHP client does, so the value included in the payload really is php_glow. Since span events are something getting deprecated in the OTel spec, we did not want to introduce a new glow type just for JavaScript.
Example
A page load that fetched some data and mounted two components:
{
"resourceSpans": [
{
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "Acme Storefront" } },
{ "key": "telemetry.sdk.language", "value": { "stringValue": "javascript" } },
{ "key": "telemetry.sdk.name", "value": { "stringValue": "@flareapp/js" } },
{ "key": "telemetry.sdk.version", "value": { "stringValue": "2.6.0" } },
{ "key": "flare.language.name", "value": { "stringValue": "javascript" } },
{ "key": "flare.framework.name", "value": { "stringValue": "react" } }
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": { "name": "@flareapp/js", "version": "2.6.0" },
"spans": [
{
"traceId": "8d4f1a2b3c5e607988d4f1a2b3c5e079",
"spanId": "1a2b3c4d5e6f7081",
"parentSpanId": null,
"name": "/products/{id}",
"startTimeUnixNano": 1710252000000000000,
"endTimeUnixNano": 1710252001240000000,
"status": { "code": 0 },
"attributes": [
{ "key": "flare.span_type", "value": { "stringValue": "browser_pageload" } },
{ "key": "flare.entry_point.type", "value": { "stringValue": "web" } },
{ "key": "flare.entry_point.value", "value": { "stringValue": "https://acme.test/products/9f2c" } },
{ "key": "flare.entry_point.handler.type", "value": { "stringValue": "browser" } },
{ "key": "flare.entry_point.handler.identifier", "value": { "stringValue": "/products/{id}" } },
{ "key": "http.route", "value": { "stringValue": "/products/{id}" } },
{ "key": "flare.route.source", "value": { "stringValue": "route" } },
{ "key": "url.full", "value": { "stringValue": "https://acme.test/products/9f2c" } },
{ "key": "document.ready_state", "value": { "stringValue": "loading" } },
{ "key": "browser.web_vital.ttfb", "value": { "doubleValue": 148 } },
{ "key": "browser.web_vital.fcp", "value": { "doubleValue": 612 } }
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0
},
{
"traceId": "8d4f1a2b3c5e607988d4f1a2b3c5e079",
"spanId": "2b3c4d5e6f708192",
"parentSpanId": "1a2b3c4d5e6f7081",
"name": "GET /api/products/9f2c",
"startTimeUnixNano": 1710252000310000000,
"endTimeUnixNano": 1710252000602000000,
"status": { "code": 0 },
"attributes": [
{ "key": "flare.span_type", "value": { "stringValue": "browser_fetch" } },
{ "key": "http.request.method", "value": { "stringValue": "GET" } },
{ "key": "url.full", "value": { "stringValue": "https://acme.test/api/products/9f2c" } },
{ "key": "server.address", "value": { "stringValue": "acme.test" } },
{ "key": "http.response.status_code", "value": { "intValue": 200 } }
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0
},
{
"traceId": "8d4f1a2b3c5e607988d4f1a2b3c5e079",
"spanId": "3c4d5e6f708192a3",
"parentSpanId": "1a2b3c4d5e6f7081",
"name": "ProductPage",
"startTimeUnixNano": 1710252000640000000,
"endTimeUnixNano": 1710252000905000000,
"status": { "code": 0 },
"attributes": [
{ "key": "flare.span_type", "value": { "stringValue": "browser_component" } },
{ "key": "flare.component.name", "value": { "stringValue": "ProductPage" } }
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0
},
{
"traceId": "8d4f1a2b3c5e607988d4f1a2b3c5e079",
"spanId": "4d5e6f708192a3b4",
"parentSpanId": "3c4d5e6f708192a3",
"name": "PriceTag",
"startTimeUnixNano": 1710252000701000000,
"endTimeUnixNano": 1710252000748000000,
"status": { "code": 0 },
"attributes": [
{ "key": "flare.span_type", "value": { "stringValue": "browser_component" } },
{ "key": "flare.component.name", "value": { "stringValue": "PriceTag" } }
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0
}
]
}
]
}
]
}
PriceTag points at ProductPage, not at the page load, because it mounted inside it.
The spans are listed parent first here so the example reads well. A real client sends them in completion order, so children come before parents. Either is fine, since Flare reads the whole payload before resolving parents. See Lifecycle.
Two vitals are added to the page span because they were measured before the pageload span closed. The other three vitals arrive later, in a second request, on a browser_web_vital span.
Every timestamp is a JSON integer. Sending one as a string is a validation error, and so is a value with a fraction left over from converting milliseconds. See Sending data.