# 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](/docs/protocol/traces/payload#span-events) and the [errors payload](/docs/protocol/errors/payload#events). For the types a PHP client sends, see [Events](/docs/protocol/general/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](/docs/protocol-javascript/traces/lifecycle). 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](/docs/protocol/general/entry-points). 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](/docs/protocol-javascript/errors/attributes#redaction) |
| `user_agent.original` | string | User agent string |
| `http.request.referrer` | string | Referrer URL, [redacted](/docs/protocol-javascript/errors/attributes#redaction) |
| `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](/docs/protocol-javascript/traces/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](/docs/protocol-javascript/errors/attributes#redaction) |
| `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](/docs/protocol-javascript/traces/lifecycle#what-a-browser-trace-looks-like) 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](/docs/protocol/general/resources), 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](/docs/protocol-javascript/traces/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](/docs/protocol/errors/payload#events), 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](https://opentelemetry.io/blog/2026/deprecating-span-events/), 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:

```json
{
    "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](/docs/protocol-javascript/traces/lifecycle#what-a-browser-trace-looks-like).

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](/docs/protocol-javascript/general/sending-data#writing-timestamps-in-javascript).
