From 866b345967c045444451ca37031e6d05e0e1525b Mon Sep 17 00:00:00 2001 From: Nitin Misra Date: Mon, 10 Aug 2026 14:32:43 +0530 Subject: [PATCH] fix(web): emit web vitals under the web.vital.* namespace (v0.1.15) The five core Web Vitals attributes were emitted without the `web.` prefix (`vital.name`, `vital.value`, `vital.rating`, `vital.id`, `vital.target_selector`) while every sibling attribute in the same block (`web.vital.cls.*`, `web.vital.inp.*`, `web.vital.lcp.*`), the emitted metrics (`web.vital.lcp`, `web.vital.fcp`, ...) and the root-span rollups (`web.vital..value`) already used it. Backends project `web.vital.name`/`value`/`rating`, so those reads came back empty and `web_vital` events rendered with no name and no measurement -- observed on session bf92606f-a07d-4a3b-8590-b268829863ac, where the spans carried correct data under the wrong keys. The old names also collided with the mobile `app_vital` schema, which owns `vital.name` and `vital.type`, putting two different schemas on one key. Attributes are asserted as literal strings rather than via `ATTR.*` so a future rename of a constant cannot keep the test green while the wire format regresses. Telemetry already ingested keeps the old attribute names. --- CHANGELOG.md | 17 +++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/core/attributes.ts | 10 +++++----- src/core/scope.ts | 2 +- src/web/instrumentations/web-vitals.test.ts | 21 +++++++++++++++++++++ 6 files changed, 47 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a85a7..37a3bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.15] - 2026-08-10 + +### Fixed + +- Web Vitals now emit their core attributes under the `web.vital.*` namespace. + `vital.name`, `vital.value`, `vital.rating`, `vital.id` and + `vital.target_selector` were missing the `web.` prefix that every sibling + attribute (`web.vital.cls.*`, `web.vital.inp.*`, `web.vital.lcp.*`), the + emitted metrics (`web.vital.lcp`, `web.vital.fcp`, …) and the root-span + rollups (`web.vital..value`) already used. Backends projecting + `web.vital.name`/`value`/`rating` read empty values, so `web_vital` events + rendered without a name or a measurement. The old keys also collided with + the mobile `app_vital` schema, which owns `vital.name` and `vital.type`. + + **Breaking for consumers of the old keys:** the previous `vital.*` names are + no longer emitted. Telemetry already ingested keeps the old attribute names. + ## [0.1.14] - 2026-08-05 ### Fixed diff --git a/package-lock.json b/package-lock.json index 624c98f..8547d04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@base-14/scout-react", - "version": "0.1.12", + "version": "0.1.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@base-14/scout-react", - "version": "0.1.12", + "version": "0.1.15", "license": "MIT", "dependencies": { "@opentelemetry/api": "^1.9.1", diff --git a/package.json b/package.json index 77d52f0..2516660 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@base-14/scout-react", - "version": "0.1.14", + "version": "0.1.15", "description": "Zero-config OpenTelemetry RUM for React and React Native. Auto-captures clicks, navigation, errors, lifecycle, network, performance, and web vitals.", "license": "MIT", "author": "base-14", diff --git a/src/core/attributes.ts b/src/core/attributes.ts index 51cb4eb..6204fd6 100644 --- a/src/core/attributes.ts +++ b/src/core/attributes.ts @@ -231,11 +231,11 @@ export const ATTR = { DEVICE_NAME: 'device.name', SERVICE_NAME: 'service.name', SERVICE_VERSION: 'service.version', - WEB_VITAL_NAME: 'vital.name', - WEB_VITAL_VALUE: 'vital.value', - WEB_VITAL_RATING: 'vital.rating', - WEB_VITAL_ID: 'vital.id', - WEB_VITAL_TARGET_SELECTOR: 'vital.target_selector', + WEB_VITAL_NAME: 'web.vital.name', + WEB_VITAL_VALUE: 'web.vital.value', + WEB_VITAL_RATING: 'web.vital.rating', + WEB_VITAL_ID: 'web.vital.id', + WEB_VITAL_TARGET_SELECTOR: 'web.vital.target_selector', WEB_VITAL_CLS_PREVIOUS_RECT_X: 'web.vital.cls.previous_rect.x', WEB_VITAL_CLS_PREVIOUS_RECT_Y: 'web.vital.cls.previous_rect.y', WEB_VITAL_CLS_PREVIOUS_RECT_WIDTH: 'web.vital.cls.previous_rect.width', diff --git a/src/core/scope.ts b/src/core/scope.ts index 5d02358..5485638 100644 --- a/src/core/scope.ts +++ b/src/core/scope.ts @@ -1,2 +1,2 @@ export const SCOPE_NAME = 'base14.scout.react'; -export const SCOPE_VERSION = '0.1.14'; +export const SCOPE_VERSION = '0.1.15'; diff --git a/src/web/instrumentations/web-vitals.test.ts b/src/web/instrumentations/web-vitals.test.ts index 06033da..33caf58 100644 --- a/src/web/instrumentations/web-vitals.test.ts +++ b/src/web/instrumentations/web-vitals.test.ts @@ -77,6 +77,27 @@ describe('installWebVitalsTracker', () => { expect(spans).toHaveLength(1); }); + // Asserted as literal keys, not via ATTR.*, because these strings are the + // wire contract the backend projects on: renaming a constant must not be + // able to keep this green. `vital.name` in particular belongs to the mobile + // app_vital schema — web vitals must not land in that namespace. + it('names the core attributes under the web.vital namespace', async () => { + await install(); + fire('LCP', 2400); + + const [span] = recorder.spans().filter((s) => s.name === SPAN.WEB_VITAL); + expect(span.attributes).toMatchObject({ + 'web.vital.name': 'LCP', + 'web.vital.value': 2400, + 'web.vital.rating': 'good', + 'web.vital.id': 'v1-LCP', + }); + expect(Object.keys(span.attributes)).not.toContain('vital.name'); + expect(Object.keys(span.attributes)).not.toContain('vital.value'); + expect(Object.keys(span.attributes)).not.toContain('vital.rating'); + expect(Object.keys(span.attributes)).not.toContain('vital.id'); + }); + // The observers cannot be torn down, so reinstalling must reuse the existing // registration rather than stacking another one. Without this, a host that // mounts the SDK n times reports every subsequent vital n times over.