Skip to content

feat(otel-web): emit browser.language and browser.timezone resource attributes#267

Open
teeohhem wants to merge 3 commits into
mainfrom
teeohhem/rum-browser-context
Open

feat(otel-web): emit browser.language and browser.timezone resource attributes#267
teeohhem wants to merge 3 commits into
mainfrom
teeohhem/rum-browser-context

Conversation

@teeohhem

@teeohhem teeohhem commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds two approximate locale/region signals to the browser RUM resource so dashboards can break traffic down by region without any collector enrichment:

  • browser.language (OTel semantic-convention attribute) from navigator.language — e.g. en-US
  • browser.timezone (IANA zone) from Intl.DateTimeFormat().resolvedOptions().timeZone — e.g. America/New_York

Why — and what this is not

These are honest proxies for where a user is. They are not IP geolocation: a browser cannot determine a visitor's country without a permission prompt (navigator.geolocation prompts and returns lat/long, which is wrong for passive RUM). True geo (geo.country.name, …) is derived in the OTel collector from the client IP via the geoip processor. This change gives a zero-permission, zero-infra region signal in the meantime, emitted on the resource so it's present on every span.

Implementation

  • New src/browserContext.ts: resolveBrowserContext() reads the environment; getBrowserContextResourceAttributes(context?) maps it to attributes. The context is injectable so the mapping is unit-testable without real browser globals, and absent values are omitted so they never overwrite a user attribute with an empty string.
  • Wired into index.ts resourceAttrs before the user-provided resourceAttributes, so callers can override either key.

Test plan

  • New test/browserContext.test.ts (added to the node mocha spec): asserts language+timezone map to the right keys, that absent values are omitted, and that resolveBrowserContext() returns a non-empty IANA zone from the real environment.
  • Verified locally: yarn workspace @hyperdx/otel-web test:unit:ci-node → all passing, including the existing Rum.init test (which now exercises the new helper at runtime). tsc --noEmit clean.

Note: @hyperdx/otel-web isn't currently wired into the aggregate nx ci:unit target (pre-existing gap), so these tests run via test:unit:ci-node rather than the broader karma suite.

Risks / rollback

Additive resource attributes, default-on but user-overridable, guarded for non-browser/Intl-less environments. The locale/UA values are already sent as HTTP headers, so no new exposure. Rollback = remove the getBrowserContextResourceAttributes() spread.

…ttributes

Add approximate locale/region signals to the browser RUM resource:
- browser.language  (OTel semconv) from navigator.language, e.g. "en-US"
- browser.timezone  (IANA zone) from Intl, e.g. "America/New_York"

These are honest proxies for a user's region, NOT IP geolocation — the
browser cannot determine country without a permission prompt; true geo
(geo.country.name, …) is derived in the collector from the client IP.

The resolver is split out (browserContext.ts) with the context injectable
so the attribute mapping is unit-tested without real browser globals, and
omits absent values so it never overwrites a user attribute with an empty
string. Wired into resourceAttrs before the user-provided
resourceAttributes so callers can override.
@changeset-bot

changeset-bot Bot commented Jun 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7c0b582

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/otel-web Minor
@hyperdx/browser Minor
@hyperdx/otel-web-session-recorder Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds two opt-out browser locale/region resource attributes — browser.language (from navigator.language) and hyperdx.browser.timezone (from Intl.DateTimeFormat) — to every span emitted by the @hyperdx/otel-web RUM SDK. Both values are emitted before user-provided resourceAttributes, preserving user override semantics.

  • packages/otel-web/src/browserContext.ts: New injectable module; reads navigator.language and Intl timezone with guards for missing globals and empty strings, keeping the mapping logic unit-testable without real browser state.
  • packages/otel-web/src/index.ts: Single-line spread of getBrowserContextResourceAttributes() at the top of resourceAttrs, ensuring user attributes and SDK_INFO still take precedence.
  • packages/otel-web/test/browserContext.test.ts: Five focused tests covering happy path, absent-value omission, empty-string filtering, live Intl timezone check, and a globalThis.navigator mock that closes the previously flagged untested code path.

Confidence Score: 5/5

Additive, default-on resource attributes with no mutations to existing behavior; user-provided attributes still override them and all environment-access paths are guarded.

The change only spreads two new attributes into the resource map before user overrides and SDK metadata. Both reads are guarded for missing globals and empty strings. The previously noted untested navigator.language code path is now covered by a globalThis mock test. No existing contracts are altered.

No files require special attention.

Important Files Changed

Filename Overview
packages/otel-web/src/browserContext.ts New module; reads navigator.language and Intl timezone with safe guards for missing globals; clean injectable design for testability.
packages/otel-web/src/index.ts Spreads getBrowserContextResourceAttributes() before user-provided resourceAttributes, giving user overrides precedence; additive change with no side effects on existing behavior.
packages/otel-web/test/browserContext.test.ts Tests cover happy path, absent-value omission, empty-string filtering, live Intl timezone, and navigator.language mock; the navigator mock addresses the previously flagged gap.
packages/otel-web/.mocharc.json Adds browserContext.test.ts to the Node mocha spec; straightforward configuration update.
.changeset/rum-browser-context.md Minor version bump for both @hyperdx/otel-web and @hyperdx/browser; appropriate since the new attributes are user-visible behavioral additions.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Rum.init()"] --> B["getBrowserContextResourceAttributes()"]
    B --> C["resolveBrowserContext()"]
    C --> D{"typeof navigator\n!== 'undefined'?"}
    D -- yes --> E["navigator.language"]
    D -- no --> F["language = undefined"]
    C --> G{"Intl available?"}
    G -- yes --> H["Intl.DateTimeFormat()\n.resolvedOptions().timeZone"]
    G -- no / throws --> I["timeZone = undefined"]
    E --> J["BrowserContext\n{ language?, timeZone? }"]
    F --> J
    H --> J
    I --> J
    J --> K{"language\ntruthy?"}
    K -- yes --> L["attrs['browser.language']"]
    K -- no --> M["omit"]
    J --> N{"timeZone\ntruthy?"}
    N -- yes --> O["attrs['hyperdx.browser.timezone']"]
    N -- no --> P["omit"]
    L --> Q["resourceAttrs spread\n(before user attrs & SDK_INFO)"]
    O --> Q
    M --> Q
    P --> Q
Loading

Reviews (3): Last reviewed commit: "refactor(otel-web): namespace the custom..." | Re-trigger Greptile

Comment thread packages/otel-web/test/browserContext.test.ts
…anches

Adds two unit tests flagged in review of the RUM browser-context change:

- getBrowserContextResourceAttributes({ language: '' }) -> {} confirms an
  empty-string language is treated as absent.
- resolveBrowserContext() reads navigator.language: mock navigator with a
  fixed 'fr-FR' locale so the truthful branch is exercised deterministically,
  independent of the host's locale (Node 22 defines navigator.language).
@teeohhem teeohhem requested a review from wrn14897 June 5, 2026 19:10
/** navigator.language, e.g. "en-US" (OTel semconv `browser.language`). */
language?: string;
/** IANA time zone, e.g. "America/New_York". */
timeZone?: string;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this customized? I don't see that in the spec https://opentelemetry.io/docs/specs/semconv/resource/browser/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — this was custom. The OTel browser resource semconv only defines browser.brands / browser.platform / browser.mobile / browser.language / browser.user_agent; there's no timezone attribute. To avoid squatting in the OTel-reserved browser. namespace (and a possible future collision), I've renamed it to hyperdx.browser.timezone in 7c0b582. browser.language stays as-is since it's the spec attribute.

Comment thread .changeset/rum-browser-context.md Outdated

Emit approximate locale/region resource attributes from the browser RUM
SDK: `browser.language` (OTel semconv, from `navigator.language`) and
`browser.timezone` (IANA zone from `Intl`). These are honest proxies for

@wrn14897 wrn14897 Jun 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timezone or timeZone?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The emitted attribute key is lowercase — now hyperdx.browser.timezone (7c0b582). The timeZone (camelCase) you may have seen is just the TypeScript field name, mirroring the JS Intl.DateTimeFormat().resolvedOptions().timeZone property — it's not the wire name. Updated the changeset to match.

…wser.timezone

Per review: the OTel browser resource semconv has no timezone attribute, so
the custom one was squatting in the reserved `browser.` namespace. Move it to
the vendor namespace `hyperdx.browser.timezone` to avoid a future collision.
`browser.language` is unchanged (it is the spec attribute). Updates the emit
site, docstring, changeset, and the two existing test assertions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants