Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cf49e1b
feat(rum-legacy): add ES5 build target and compatibility gate
Fiona2016 Aug 15, 2026
4d3a115
feat(rum-legacy): send batched events over XMLHttpRequest
Fiona2016 Aug 15, 2026
e5f37ee
feat(rum-legacy): collect errors, page load timings and views
Fiona2016 Aug 15, 2026
8949a52
feat(rum-legacy): expose the full public API surface
Fiona2016 Aug 15, 2026
a7933ac
test(rum-legacy): verify behaviour without the modern browser APIs
Fiona2016 Aug 15, 2026
33c455e
refactor(rum-legacy): remove duplicated object and error helpers
Fiona2016 Aug 16, 2026
5999eee
fix(rum-legacy): send a closing view update when the page unloads
Fiona2016 Aug 16, 2026
e30b7af
fix(rum-legacy): apply sessionSampleRate and honour trackingConsent
Fiona2016 Aug 17, 2026
11ffdfe
fix(rum-legacy): guard browser callbacks and fix in-page referrer
Fiona2016 Aug 17, 2026
4fd8414
fix(rum-legacy): correct wall-clock durations, exit ordering and sess…
Fiona2016 Aug 17, 2026
721024c
fix(rum-legacy): recognise errors from other frames, stop leaking state
Fiona2016 Aug 17, 2026
e7b8786
fix(rum-legacy): copy context and configuration on the way in as well
Fiona2016 Aug 17, 2026
b7c78c0
fix(rum-legacy): reject a non-numeric sample rate, keep foreign cooki…
Fiona2016 Aug 17, 2026
8738d26
fix(rum-legacy): declare the content type the intake requires
Fiona2016 Aug 17, 2026
87a46da
feat(rum-legacy): add a real-browser verification harness
Fiona2016 Aug 18, 2026
2963991
fix(rum-legacy): make the verification run fit a metered device session
Fiona2016 Aug 18, 2026
31a73b8
feat(rum-legacy): guarantee silence on engines below the support floor
Fiona2016 Aug 18, 2026
2eaced7
fix(rum-legacy): harden the verification harness on real Trident engines
Fiona2016 Aug 18, 2026
ce49713
fix(rum-legacy): survive the formatter and the pre-XHR engines in the…
Fiona2016 Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/deploy-auto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
- name: Build bundle
run: yarn build:bundle

- name: Verify ES5 compatibility of the legacy bundle
run: yarn test:compat:es5

- name: Deploy to prod
run: node ./scripts/deploy/deploy-oss.js prod v${VERSION}
env:
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/deploy-manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ jobs:
- name: Build bundle
run: yarn build:bundle

- name: Verify ES5 compatibility of the legacy bundle
run: yarn test:compat:es5

- name: Deploy to prod
run: node ./scripts/deploy/deploy-oss.js prod v${VERSION}
env:
Expand Down Expand Up @@ -67,7 +70,7 @@ jobs:
run: node ./scripts/deploy/publish-npm.js
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

notify-success:
needs: publish-npm
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ jobs:
- name: Build bundle
run: yarn build:bundle

- name: Verify ES5 compatibility of the legacy bundle
run: yarn test:compat:es5

- name: Deploy to staging
run: node ./scripts/deploy/deploy-oss.js staging v${VERSION}
env:
Expand Down
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ rum-events-format
developer-extension/dist
test/**/dist
yarn.lock
# IE8 counts a trailing comma in an array literal as an extra undefined element, and prettier
# insists on adding them; this page must stay runnable down to IE6.
packages/rum-legacy/verification/index.html
1 change: 1 addition & 0 deletions eslint-local-rules/disallowSideEffects.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const pathsWithSideEffect = new Set([
`${packagesRoot}/flagging/src/entries/main.ts`,
`${packagesRoot}/rum/src/entries/main.ts`,
`${packagesRoot}/rum-slim/src/entries/main.ts`,
`${packagesRoot}/rum-legacy/src/entries/main.ts`,
])

// Those packages are known to have no side effects when evaluated
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"test:e2e:ci": "yarn test:e2e:init && yarn test:e2e",
"test:e2e:ci:bs": "yarn build && yarn build:apps && yarn test:e2e:bs",
"test:compat:tsc": "node scripts/check-typescript-compatibility.js",
"test:compat:es5": "node scripts/check-es5-compatibility.js",
"test:compat:ssr": "scripts/cli check_server_side_rendering_compatibility",
"rum-events-format:sync": "scripts/cli update_submodule && scripts/cli build_json2type && node scripts/generate-schema-types.js",
"size": "node scripts/show-bundle-size.js",
Expand All @@ -49,6 +50,7 @@
"@types/express": "5.0.2",
"@types/jasmine": "3.10.18",
"@types/node": "22.15.19",
"acorn": "8.14.1",
"ajv": "8.17.1",
"ali-oss": "6.22.0",
"browserstack-local": "1.5.6",
Expand Down
209 changes: 209 additions & 0 deletions packages/rum-legacy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# RUM Browser SDK — legacy build

A separate, self-contained build of the RUM Browser SDK for browsers without ES2015 support.

The standard bundles are compiled to ES2018 and send over `fetch` / `sendBeacon`. On a browser that
supports neither, the script fails to parse before any code inside it runs, so no amount of feature
detection in the SDK can help. This package is the answer to that: a smaller SDK, compiled to ES5,
that sends over `XMLHttpRequest`.

It is distributed through the CDN only and is not published to npm. Bundling it with an application
would put its output back into a file the browser has to parse as a whole, which is the failure this
build exists to avoid.

## What it collects

| Capability | Supported | Notes |
| -------------------------- | :-------: | ------------------------------------------------ |
| Uncaught JavaScript errors | ✅ | No stack; the script url and line are reported |
| Page load timings | ✅ | From `performance.timing` |
| Views | ✅ | Initial load, plus `hashchange` and manual views |
| Manual actions and errors | ✅ | `addAction`, `addError` |
| Session and user identity | ✅ | Same session cookie as the standard bundles |
| Resource timings | ❌ | No Resource Timing API |
| Automatic user actions | ❌ | Requires DOM observation not available here |
| Web Vitals, long tasks | ❌ | No `PerformanceObserver` |
| Session replay | ❌ | No `MutationObserver` |
| CSP violation reporting | ❌ | No `securitypolicyviolation` event |

Everything unsupported is a no-op method rather than a missing one. A page written against the
standard bundle runs unchanged; it does not need to branch on the browser.

Below the floor — IE6 to IE8 and their document modes, which the loader snippet also routes here —
the promise inverts: nothing is collected, and the bundle's whole evaluation is guarded so the
hosting page stays untouched. `Object.defineProperty` on plain objects, which IE8 rejects, is
guarded individually, and the build gate additionally rejects ES3 reserved words used as property
names, which those engines cannot even parse and no runtime guard could catch.

## Setup

Both builds share the `FC_RUM` global and the same call sequence, so the page carries one snippet.
The choice is made on capability, not on the user agent string, which means a browser running in a
compatibility document mode is classified by what it can actually do.

```html
<script>
;(function (w, d) {
var legacy = typeof w.Promise !== 'function' || typeof w.fetch !== 'function'
w.FC_RUM = w.FC_RUM || {
q: [],
onReady: function (c) {
this.q.push(c)
},
// Stub so that calling init before the script has landed queues the call instead of throwing
// "undefined is not a function" and taking the page down with it.
init: function (o) {
this.q.push(function () {
w.FC_RUM.init(o)
})
},
}
var s = d.createElement('script')
s.async = true
s.src = legacy ? 'https://<static host>/fc-rum-legacy.js' : 'https://<static host>/flashcat-rum.js'
d.getElementsByTagName('head')[0].appendChild(s)
})(window, document)
</script>
<script>
window.FC_RUM.onReady(function () {
window.FC_RUM.init({
applicationId: '<application id>',
clientToken: '<client token>',
proxy: '/rum-intake/',
})
})
</script>
```

Calls made before the bundle arrives are queued on `q` and run once it loads. This is the same
mechanism the standard bundles already use. `init` is stubbed on the placeholder for the same
reason: a page that calls it outside `onReady`, before the script has landed, would otherwise hit
`undefined is not a function` — the failure this build exists to prevent.

### `proxy` is required

`proxy` is a path on the page's own origin that the customer's web server forwards to the intake.
It is not optional here, unlike in the standard bundles, because these browsers cannot make a
cross-origin `XMLHttpRequest` carrying the parameters the intake needs. `init` reports the problem
and collects nothing rather than sending requests that would be blocked.

The request is shaped exactly like the one the standard bundles send, so a single reverse proxy rule
serves both and the intake needs no compatibility branch:

```
POST https://<page origin>/rum-intake/?ddforward=<url-encoded /api/v2/rum?...>
```

An nginx rule forwarding it, for example:

```nginx
location /rum-intake/ {
proxy_pass https://<intake host>/;
}
```

The request declares `Content-Type: text/plain;charset=UTF-8`, which the intake requires. The
standard bundles never declare it because `fetch` and `sendBeacon` set it implicitly for a string
body; `XMLHttpRequest` on these browsers cannot be relied on to do the same. It costs nothing: the
request is same-origin, and `text/plain` is a safelisted value that does not trigger a preflight
even when it is not.

If a Content Security Policy is in force it needs to allow the static host and `connect-src` to the
page's own origin. `unsafe-eval` is not required.

## Configuration

| Option | Required | Notes |
| ------------------- | :------: | ----------------------------------------------------------------------------- |
| `applicationId` | ✅ | |
| `clientToken` | ✅ | |
| `proxy` | ✅ | Same-origin path forwarded to the intake |
| `service` | | |
| `version` | | |
| `env` | | |
| `sessionSampleRate` | | 0 to 100, defaults to 100. Decided once per session and carried in the cookie |
| `trackingConsent` | | `granted` (default) or `not-granted`; any other value counts as not granted |

Options that only apply to the standard bundles are accepted and ignored, so one configuration
object can be shared between the two.

## Differences from the standard bundles

Beyond the capability table above, two behaviours differ and are worth knowing before porting a
page:

- `stopSession()` shuts collection down for the rest of the page. In the standard bundles it ends
the current session and a new one starts on the next interaction. Use `setTrackingConsent` if you
want collection to be resumable.
- `setViewName()` starts a new view rather than renaming the current one. A view event has already
been sent under the old name and there is no way to retract it.

Consent is honoured: with `trackingConsent: 'not-granted'` nothing is collected or sent, and
withdrawing consent later drops whatever is buffered and clears the session cookie.

## Development

```bash
yarn build:bundle # typecheck, bundle, then verify ES5 compatibility
yarn typecheck # ES5 lib check on its own
```

`tsconfig.json` deliberately does not extend the repository base config. `lib` is restricted to
`ES5` and `DOM` so that using an API the target browsers lack is a compile error rather than a
runtime crash, and `paths` is emptied so `@flashcatcloud/*` imports do not resolve — those packages
are written against ES2018 and importing one would defeat the purpose of this build.

Two checks run as part of the bundle build. `scripts/check-es5-compatibility.js` parses the output
as ES5, scans it for runtime APIs the target browsers lack, and asserts that the standard bundles
are _rejected_, so a broken check cannot pass silently.

`scripts/check-legacy-bundle-runtime.js` then executes the emitted file in a deliberately
impoverished environment — no `fetch`, no `Promise`, no `sendBeacon`, and an `XMLHttpRequest` that
only fires `onreadystatechange` — and asserts what lands on the wire: a synchronous POST, the intake
path and parameters inside `ddforward`, and a payload carrying a view and an error. Every unit spec
runs against TypeScript compiled by the test runner; between that and the shipped file sit Terser
and the webpack runtime, and this is what covers the gap.

## Testing, and what it does not cover

The specs run in a modern headless browser. `src/boot/degradedEnvironment.spec.ts` removes `fetch`,
`Promise`, `MutationObserver`, `PerformanceObserver`, `TextEncoder`, `URL` and `sendBeacon`, and
drives the package end to end through an `XMLHttpRequest` that offers only `onreadystatechange`, as
IE9 does.

The ES2015 collections are deliberately left in place there. `lib: ES5` already makes using them a
compile error, which is stronger than a runtime spec, and the bundle scan covers the emitted output.
Removing them at runtime would only break the test harness, which builds a `Map` of its own around
every listener.

Guarantees that could be asserted vacuously are checked by removing the implementation and
confirming a spec fails: the ES5 gate, the event schema validation, the page exit ordering, the
sampling and consent gates, and the listener guards.

That covers missing runtime APIs and unsupported syntax. It does not cover the behaviour of an
actual old browser engine. **This package has not been verified on real hardware**, and that
verification is a separate step before any support commitment is made.

## Verifying on a real browser

`verification/` holds a self-contained harness for exactly that step:

```bash
node packages/rum-legacy/scripts/verification-server.js # builds are not included: build first
```

Then open `http://localhost:8099/` in the browser under test and press _Run checks_. The page is
plain ES5 and renders every result into the DOM, because the browsers it targets often have no
usable developer tools. The server doubles as a same-origin intake that records what actually
arrived — method, content type, body — so the checks assert the wire, not the SDK's own claims:
the bundle loads, `init` and the collection APIs do not throw into the page, an uncaught error
still reaches the page's own handler, the session cookie is written, and the intake received a
`text/plain` POST whose real path travels inside `ddforward`, carrying a view and an error event.

On Windows, Edge's IE mode (F12 → emulation → document mode 9/10/11) runs the real Trident engine
and is the cheapest meaningful pass; a run on actual IE hardware or a cloud device farm is the
authoritative one. Two checks only have meaning on a real Trident engine, which is precisely why they are in this
page and not only in the unit suite: the content-type assertion passes on any modern browser
regardless of the SDK, because `fetch`-era browsers add the header to a string body implicitly —
and the page-exit assertion shows SKIP on modern engines, which block synchronous XHR during page
dismissal by design.
27 changes: 27 additions & 0 deletions packages/rum-legacy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@flashcatcloud/browser-rum-legacy",
"version": "0.0.2",
"license": "Apache-2.0",
"private": true,
"description": "RUM Browser SDK build for browsers without ES2015 support. Distributed through the CDN only.",
"scripts": {
"build": "yarn build:bundle",
"build:bundle": "rm -rf bundle && yarn typecheck && SDK_SETUP=cdn webpack --mode=production && yarn check:es5 && yarn check:runtime",
"check:es5": "node ../../scripts/check-es5-compatibility.js",
"check:runtime": "node ../../scripts/check-legacy-bundle-runtime.js",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"devDependencies": {
"ajv": "8.17.1",
"terser-webpack-plugin": "5.3.14",
"webpack": "5.99.8"
},
"repository": {
"type": "git",
"url": "https://github.com/flashcatcloud/browser-sdk",
"directory": "packages/rum-legacy"
},
"volta": {
"extends": "../../package.json"
}
}
Loading
Loading