Skip to content

feat(rum-legacy): ES5 build for browsers without ES2015 support - #22

Merged
Fiona2016 merged 32 commits into
mainfrom
feat/rum-legacy-es5
Aug 21, 2026
Merged

feat(rum-legacy): ES5 build for browsers without ES2015 support#22
Fiona2016 merged 32 commits into
mainfrom
feat/rum-legacy-es5

Conversation

@Fiona2016

@Fiona2016 Fiona2016 commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

What

Adds packages/rum-legacy, a separate CDN-only 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 inside the SDK can help. This build is compiled to ES5 and sends over XMLHttpRequest.

It is self-contained: it does not import @flashcatcloud/browser-core or browser-rum-core, which are authored against ES2018. Nothing in the existing packages changes.

Capabilities

Uncaught errors, page load timings from performance.timing, views (initial load, hashchange, manual), manual actions and errors, and session/user identity.

Resource timings, automatic user actions, Web Vitals, long tasks, session replay and CSP reporting are not available: the underlying platform APIs do not exist on these browsers. Those methods are present as no-ops rather than absent, because a missing method throws undefined is not a function and takes the host page down, which is the failure this build exists to prevent. A page written against the standard bundle runs unchanged.

Design notes

One reverse proxy rule serves both builds. The intake URL is built to match the standard bundles byte for byte, so no compatibility branch is needed on the intake. Two details carry that and are easy to get wrong: the real intake path travels inside the ddforward query parameter rather than being appended to the proxy path, and a relative proxy value is resolved to an absolute URL first. The specs build a reference URL with the standard implementation and compare against it, so a change on either side fails loudly instead of drifting.

proxy is required here, unlike in the standard bundles. These browsers cannot make a cross-origin XMLHttpRequest carrying the parameters the intake needs, so init reports the problem and collects nothing rather than sending requests that would be blocked.

The content type is declared explicitly. The intake rejects a body that is not text/plain. The standard bundles never declare it because fetch and sendBeacon set it implicitly for a string body, but XMLHttpRequest on these browsers cannot be relied on to do the same, so without it every request would be refused. 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.

Completion is detected through onreadystatechange. onload arrived in IE10, so a transport built on it looks correct in a modern test browser and never completes where this package runs. The spec's fake XMLHttpRequest fires only onreadystatechange to keep that honest. The exit path sends synchronously, as there is no sendBeacon.

sessionSampleRate is decided once per session and carried in the session cookie, so a session is either collected whole or not at all rather than losing a fraction of the events of every session.

trackingConsent is honoured. Collection runs only while consent is exactly granted, matching the standard bundles, where an unrecognised value counts as not granted. Withdrawing consent drops whatever is buffered rather than sending it, and clears the session cookie.

Session identity reuses the existing cookie, serialisation and expiration rules. The existing parser rejects uppercase characters, so the generated uuid stays lowercase or every page load would silently start a new session. A session the standard bundles started is honoured as tracked whether or not replay was sampled: both builds share one cookie jar per domain, and IE enterprise site lists routinely put some urls of a site in compatibility mode and others not.

Durations come from the wall clock, since these browsers have no monotonic performance.now(). A clock correction can move time backwards mid-view, so elapsed time is floored at zero rather than reported negative, and the session throttle treats a backwards jump as an elapsed window rather than freezing.

Payload size is measured with a UTF-8 byte count rather than string length, which would undercount non-latin content threefold and let batches grow past the intake limit.

Timers and listener registration go through the unpatched originals when Zone.js is present, whose patched versions have been observed to cause memory leaks and high CPU usage in host pages.

Session cookie access is throttled to one second, as the standard bundles throttle it. The session is looked up for every event, and reading and writing document.cookie is a full string parse each time.

Both entry points are guarded: public methods and the handlers the browser calls back into. A failure inside a listener would otherwise become an uncaught error on the page, which is the outcome this package exists to avoid.

The page exit is one atomic sequence. The closing view update is produced inside the exit flush, so a buffer limit it happens to cross cannot start an asynchronous request that the closing page would never complete.

Time is read via a local dateNow() rather than Date.now(), which some sites wrongly polyfill to return a Date instance. Pages still running these browsers are the most likely to carry such a dependency.

Guardrails

packages/rum-legacy/tsconfig.json deliberately does not extend the base config. lib is restricted to ES5 + DOM, which turns using an unavailable API into a compile error rather than a runtime crash, and paths is emptied so @flashcatcloud/* imports do not resolve.

scripts/check-es5-compatibility.js runs as part of the bundle build and in the deploy workflows. It parses the output as ES5, scans it for runtime APIs these browsers lack, and asserts that the standard bundles are rejected — if a misconfiguration made the parser accept everything, the positive assertion alone would still pass and the gate would silently stop protecting anything.

Testing

166 specs. src/boot/degradedEnvironment.spec.ts removes fetch, Promise, the observers, TextEncoder, URL and sendBeacon, then drives the package end to end. Event shape is validated against the shared rum-events-format schemas rather than hand-written expectations.

The full unit suite is unchanged from main — the same set of failing specs before and after, all pre-existing.

A second check executes the emitted bundle in an environment with no fetch, no Promise, no sendBeacon and an XMLHttpRequest that only fires onreadystatechange, then asserts what lands on the wire. The specs run against TypeScript compiled by the test runner; Terser and the webpack runtime sit between that and the shipped file. It caught a real defect on its first run: errors were recognised with a bare instanceof Error, so an error created in another frame was stringified and lost its message, type and stack — and frameset-heavy applications are the norm on these browsers.

Guarantees that are easy to assert vacuously were 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, the listener guards, and the intake path carried inside ddforward.

Not covered

Verified on real Trident engines (BrowserStack Live, Windows 7): IE10 and IE11 pass the full harness; IE9 passes including the synchronous page-exit request that only a real Trident engine can prove. Below the floor, IE6-IE8 get a guaranteed silent no-op: the whole evaluation is guarded and the build gate rejects ES3-reserved-word property names those engines cannot parse. Historical caveat, superseded by those runs: The specs cover missing runtime APIs and unsupported syntax; they do not cover the behaviour of an old browser engine. That verification is a separate step before any support commitment.

The package is not published to npm, and the public compatibility documentation is unchanged.

Introduce packages/rum-legacy, a CDN-only bundle for browsers without
ES2015 support. This commit sets up the toolchain only; collection and
transport follow.

The package does not extend tsconfig.base.json on purpose. Restricting
"lib" to ES5 + DOM turns a missing runtime API into a compile error
rather than a crash on the target browsers, and an empty "paths" map
keeps @flashcatcloud/* imports unresolvable, since those packages are
authored against ES2018.

check-es5-compatibility.js parses the emitted bundle with acorn at
ecmaVersion 5. It also asserts that the modern bundles are rejected: if
a misconfiguration made the parser accept everything, the positive
assertion alone would still pass and the gate would silently stop
protecting anything.

Console access is looked up lazily instead of captured at module
evaluation, because in IE9 window.console does not exist until the
developer tools are opened, and its methods are host objects without
bind().
Transport for browsers that have neither fetch nor sendBeacon.

The intake url is built to match the modern bundle byte for byte, so a
single reverse proxy rule on the customer domain serves both builds and
the intake needs no compatibility branch. Two details carry that
property and are easy to get wrong: the real intake path travels inside
the ddforward query parameter rather than being appended to the proxy
path, and a relative proxy value is resolved to an absolute url first.
The specs build a reference url with the modern implementation and
compare against it, so a change on either side fails loudly instead of
drifting.

Completion is detected through onreadystatechange. onload arrived in
IE10, so a transport built on it would look correct in a modern test
browser and never complete on the browsers this package exists for. The
spec's fake XMLHttpRequest fires only onreadystatechange to keep that
honest. The exit path sends synchronously because there is no
sendBeacon to hand the payload to.

Batch limits match the modern bundle. Payload size is measured with a
UTF-8 byte count rather than string length, which would undercount
non-latin content threefold and let batches grow past the intake limit.

Session identity reuses the modern cookie name, serialisation and
expiration rules. The modern parser rejects uppercase characters, so the
generated uuid has to stay lowercase or every page load would silently
start a new session.

Timers and listener registration go through the unpatched originals when
Zone.js is present, whose patched versions have been observed to cause
memory leaks and high CPU usage in host pages.

Time is read via a local dateNow() rather than Date.now(), which some
sites wrongly polyfill to return a Date instance. Pages still running
these browsers are the most likely to carry such a dependency.
Adds the event assembly and the collection this build can actually
support: uncaught errors, page load timings, view lifecycle and manual
actions.

Event shape is validated in the specs against the shared
rum-events-format schemas rather than against hand-written
expectations, since the intake owns that format. Durations are
nanoseconds, so page load timings derived from performance.timing are
converted rather than passed through as milliseconds.

The zero-valued resource and long task counts are emitted rather than
omitted. Those signals cannot be observed on these browsers, and
leaving the fields out would read downstream as missing data instead of
a real zero. Timings the browser has not reached are the opposite case:
performance.timing reports them as 0, which would be a false
measurement, so they are left out.

window.onerror preserves and still calls whatever handler the page had
installed, and passes its return value back so the page can keep
suppressing the browser's default logging. Replacing it outright would
silently disable the customer's own error reporting. Without an error
object there is no stack, so the script url and line are folded into a
single synthetic frame, which is what makes the error locatable at all.

Route changes are tracked through hashchange only, as there is no
History API to hook into here.
Wires collection, assembly, batching and transport behind the same
FC_RUM surface the modern bundle exposes.

Methods that cannot be supported here are no-ops rather than absent.
There is no PerformanceObserver for vitals, no MutationObserver for
session replay and no way to observe resource timings, but a missing
method throws "undefined is not a function" and takes the host page
down, which is the failure this package exists to prevent. A page
written against the modern bundle therefore runs unchanged.

Every public method is wrapped so an internal failure cannot surface as
an exception in the page. onReady is deliberately left unwrapped: it
invokes the caller's own callback, and swallowing there would hide the
customer's exceptions rather than ours.

The view context is passed to the view update callback rather than read
back from the manager. The first update is emitted while the manager is
still being constructed, so reading it back threw and, being caught by
the safety net, silently produced no events at all.

Uncaught and manually added errors share one path, so an error is
counted and reported exactly once.

The bundle size grows from 505 bytes to 39 KiB of sources, still
parsing as ES5.
Adds a fixture that removes fetch, Promise, sendBeacon, the observers,
TextEncoder and the URL constructor, then drives the package end to end
through an XMLHttpRequest offering only onreadystatechange. Without it
every spec runs in a browser that has all of those, so a dependency on
one would pass the suite and fail only where this package is meant to
run.

The ES2015 collections are deliberately left in place. lib: ES5 already
makes using them a compile error, a stronger guarantee than a runtime
spec, and the bundle scan covers the emitted output. Removing them here
broke the suite's own instrumentation instead: the shared leak detector
wraps addEventListener in a function that constructs a Map, so the first
listener this package registered failed inside the harness rather than
inside the code under test.

Globals are restored by putting back the captured property descriptor,
and a shadow over an inherited property is deleted rather than
overwritten. Restoring navigator.sendBeacon by assignment left it as an
own property of the instance rather than a method on Navigator.prototype,
which changed its shape for every later spec in the same browser context
and failed 41 of them across other packages.

check-es5-compatibility.js now also scans the bundle for runtime APIs
the target browsers lack. Parsing as ES5 says nothing about those: a
bundle full of Promise and fetch parses perfectly well and then fails on
the first line that runs.

Adds a package README covering setup, the required same-origin proxy,
the capability matrix, and an explicit statement that this has not been
verified on real hardware.
The merge and empty-check loops existed twice, byte for byte, in event
assembly and in the public api, because Object.assign and the spread
operator both need ES2015 and lib: ES5 rejects them. They move to
tools/objectUtils.ts.

The block reading a message, name and stack off an Error instance also
existed twice in error collection, once for uncaught errors and once
for manually added ones.

No behaviour change.
The view event carrying the time spent and the error and action counts
was only sent when the session was stopped explicitly. On a normal page
close nothing closed the view, so every view reached the intake with the
counts and duration it had at page load, which are zero. Error events
themselves were unaffected; the view level aggregates were not.

Emitting it was not enough on its own. The batch registered its own exit
listener when it was created, before the view manager existed, so it
always ran first and flushed an empty buffer before the closing update
could be added to it. Page exit is now owned in one place, which closes
the view and then flushes, and the batch no longer listens for it.

The exit path closes the view without shutting collection down.
beforeunload can fire for a navigation the user then cancels, and
tearing down there would leave the page with a dead SDK. It also runs
once per page: the request it makes is synchronous, and blocking a
closing browser twice is worse than missing a second closing update on
a cancelled navigation.

viewManager.flush() is replaced by endView(). It had no caller outside
its own specs.
Both options were accepted, validated and then ignored.

sessionSampleRate only reached _dd.configuration.session_sample_rate.
Every session was collected in full while each event claimed to have
been sampled at the configured rate, so the volume was wrong and the
reported rate described something that never happened. The decision is
now made once when a session starts and carried in the session cookie's
rum field, using the same values as the standard bundles, so a session
is either collected whole or not at all rather than losing a fraction
of each one.

trackingConsent was a no-op, which is worse for a consent control than
not offering it: a page could set 'not-granted' and still be collected
from. Collection now runs only while consent is exactly 'granted',
matching the standard bundles, where an unrecognised value counts as not
granted. Withdrawing consent drops whatever is buffered instead of
sending it and clears the session cookie.

Session cookie access is throttled to one second, as the standard
bundles throttle it. The session is looked up for every event, and
reading and writing document.cookie is a full string parse each time,
which is a cost worth avoiding on the browsers this package targets.
Public methods were wrapped so an internal failure could not surface in
the host page, but the handlers the browser calls back into were not.
A failure inside the hashchange, load or page exit listener became an
uncaught error on the page, which is the outcome this package exists to
avoid. The wrapper moves to tools/monitor.ts and now covers both entry
points.

Removing the wrapper failed no test before this change, so the guard was
untested rather than merely missing; the specs added here fail without
it. Making them fail for the right reason also required advancing past
the new session cookie throttling window, since a page that exits within
a second of init never touches the cookie and never reaches the
injected failure.

Views started in-page reported document.referrer, which describes how
the document was reached rather than how the view was, attributing every
in-page navigation to whatever site linked to the page. They now report
the previous view's url, as the standard bundles do, and only the first
view of a document falls back to document.referrer.

The loader snippet stubs init so that calling it outside onReady, before
the script has landed, queues the call instead of throwing "undefined is
not a function". The README also records where this build's stopSession
and setViewName deliberately differ from the standard bundles.

Session cookies are cleared before each spec as well as after: a spec
elsewhere may leave one behind, and a stale session would be reused
instead of a fresh one being created.
…ion reuse

Five more review passes, over clock behaviour, ordering, hostile input,
release plumbing and drift between the docs and the code.

Durations come from the wall clock, because these browsers have no
monotonic performance.now(). A backwards clock correction made
time_spent negative, which is not a measurement but a broken one, and
made the session throttle read the negative elapsed time as "still
inside the window", freezing the session until the clock caught up.
Elapsed time is now floored at zero and the throttle treats a backwards
jump as an elapsed window.

The page exit produced the closing view update and then flushed. If that
update crossed a buffer limit it started an asynchronous request, which a
closing page never completes. The update is now produced inside the exit
flush, so the whole sequence stays on the synchronous transport.

A session started by the standard bundles with session replay sampled is
stored as '1' rather than '2'. Reading only '2' as tracked meant such a
session was treated as sampled out and silenced for its whole lifetime.
Both builds share one cookie jar per domain, and IE enterprise site
lists routinely put some urls of a site in compatibility mode and others
not, so this is reachable rather than theoretical.

Hostile input was probed rather than assumed: a crafted session cookie,
a polluted Object prototype and a malformed cookie value are all
contained already, and now have specs saying so.

The bundle whose whole purpose is being small was missing from the size
report. It is 4 KiB gzipped.

The README claimed the degraded environment specs remove Map, Set and
Symbol. They deliberately do not, and overstating the coverage is worse
than describing it narrowly.
Four more review passes, over the emitted artifact, the module surface,
the changes outside this package, and the public API semantics.

The suite never executed the file customers actually load. Every spec
runs against TypeScript compiled by the test runner, and between that
and the shipped bundle sit Terser and the webpack runtime. A new check
executes the emitted file in an environment with no fetch, no Promise,
no sendBeacon and an XMLHttpRequest that only fires onreadystatechange,
then asserts what lands on the wire, including the intake path and
parameters carried inside ddforward.

It found a real defect on its first run. Errors were recognised with a
bare `instanceof Error`, which compares against the current frame's
constructor, so an error created in another frame was treated as a plain
value and stringified, losing its message, type and stack. Frameset and
iframe heavy applications are the norm on these browsers. The standard
bundles allow for this and now so does this one, verified with a real
iframe rather than a simulation.

The getters handed out the objects the SDK keeps rather than copies. The
stored configuration is what a later consent grant starts from, and the
contexts are attached to every event, so a caller could change SDK
behaviour by mutating what it read.

computeBytesCount and normalizeUrl were exported without a consumer,
which reads as part of the module's contract when they are internal.

The root build, the deploy path's package list and the workflow's ES5
step were run end to end rather than assumed.
The previous pass stopped the getters handing out the objects the SDK
keeps, but left the other half: setGlobalContext, setUser, setAccount
and init all stored the caller's object by reference.

Pages commonly keep the object they passed. An unrelated later mutation
of it silently changed what every subsequent event carried, and for the
configuration it changed what a later consent grant would start from.
Fixing only the read side left the same defect reachable from the write
side, which is worse than not having noticed it, because the specs
looked like the problem was covered.

Data is now copied at both boundaries.
…e fields

The sample rate range was checked by negating it. NaN fails every
comparison, so a rate computed from a string and landing on NaN passed
validation, and then failed the sampling comparison too: the SDK looked
configured and silently reported nothing, which is the worst way for a
monitoring build to go wrong. The range is now checked positively, as
the standard bundles check it.

The session cookie is shared with the standard bundles, which keep their
own entries in it. The anonymous user id is one of them and is tracked
by default. Rewriting the cookie with only the four fields this build
understands destroyed it, so a visit through a page served in
compatibility mode reset anonymous user continuity for every other page
of the same site. Entries this build does not understand are now written
back untouched; they still cannot reach the session identity or the
tracking decision, which are read from named fields only.
Verified the "no backend change" claim against the intake itself rather
than against the standard bundles' url shape, and found the transport
would have been refused outright.

The intake rejects any body whose content type is not text/plain. This
build deliberately set no request header at all, on the reasoning that
it kept the request simple and avoided a preflight. Both halves of that
were wrong: a same-origin request never preflights, and text/plain is a
safelisted value that does not trigger one even cross-origin. The
standard bundles get away with declaring nothing because fetch and
sendBeacon set it implicitly for a string body; XMLHttpRequest on these
browsers cannot be relied on to do the same.

Nothing client-side could have caught this. The specs and the artifact
check both asserted the absence of headers, so the mistaken belief was
encoded three times over: in the transport, in its spec, and in the fake
XMLHttpRequest of the degraded environment specs, which threw if a
header was set.

Both levels now assert the header, and both fail without it.
Everything this package is checked with so far runs on a modern engine:
the unit suite, the degraded-environment specs and the artifact smoke
test all approximate the target browsers rather than being one. This
adds the missing step, a harness for running the shipped bundle on a
real browser and seeing the result on the device itself.

The page is plain ES5 and renders every check 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, so
the checks assert the wire rather than the SDK's own claims: the bundle
loads, 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.

One check only has teeth on an old engine: any fetch-era browser adds
the content type to a string body implicitly, so the header assertion
cannot fail there regardless of the SDK. That is exactly why it lives in
this harness and not only in the unit suite.

JSON is parsed with JSON.parse, native since IE8. An eval-based parse
would also break under any Content Security Policy, which the rest of
the package promises not to require.
Cloud device farms meter free sessions by the minute, and the harness
spent over thirty seconds waiting out the SDK's flush timer. The run now
fills the batch to its limit so it flushes over the asynchronous path
immediately, starts itself when opened with ?autorun=1, and keeps its
results across the exit-check reload in sessionStorage. A full pass
takes under a second plus one reload.

The root path did not resolve when a query string was attached, which
made ?autorun=1 a 404: routing now matches on the pathname.

The page-exit check reports SKIP rather than FAIL on modern engines,
which block synchronous XHR during page dismissal by design. Like the
content-type check, it can only genuinely pass or fail on Trident, which
is why it is in this page at all.
Real-device runs surfaced what happens below IE9: the loader snippet
routes every browser without fetch and Promise to this bundle, and on
IE8 the whole evaluation died on Object.defineProperty, which rejects
plain objects there. The throw surfaced as an uncaught error in the
hosting page, and IE8 document mode is routinely forced by enterprise
site lists, so this is reachable, not theoretical.

The promise for those engines inverts: collecting nothing is fine, but
the page must stay untouched. The defineProperty call now falls back to
a plain assignment, the entire module evaluation is guarded so any
construction failure leaves the loader's queued stub in place, and a
spec holds the constructor to that with a throwing defineProperty.

Syntax cannot be guarded at runtime, so the build gate now also parses
the bundle for ES3 reserved words used as property names, which the
IE6/7 engines fail to parse outright. ES5 allows them, meaning neither
the compiler nor the ES5 parse check would object.
Real IE runs found four defects in the harness itself. Tables are now
built with DOM calls: IE9 makes innerHTML read-only on table sections
and IE8 rejects it with its own error, so string rendering worked
everywhere except on the devices this page exists for. The bundle
loader guards its callback, because IE10 and 11 fire both onload and
onreadystatechange and every check ran twice. Payload assertions
aggregate all received requests, since async flushes travel the tunnel
independently and arrive out of order. A rerun stops the previous SDK
instance first instead of leaving two instances reporting at once.

Errors now surface into the page from a separate script block that
survives a syntax error in the main one, which is what identified every
failure above on consoleless browsers. A final check asserts that no
unexpected uncaught error reached the page, which is the whole
acceptance criterion for engines below the support floor. The server
logs each request so the device's traffic is observable from the
serving side.
… harness

Three more findings from real IE6 and IE8 runs.

The formatter added trailing commas to multiline literals. They are
legal ES5, so every static check passed, but IE8 counts a trailing
comma in an array literal as one more undefined element, and IE6 and 7
refuse to parse them in object literals at all. The page is now listed
in .prettierignore, carries a comment saying why, and the commas are
gone.

IE6 predates the native XMLHttpRequest constructor, so the harness's
own requests threw before they could observe anything. It now falls
back to the ActiveX flavour, which ran successfully on a real MSIE 6.0.

The no-unexpected-errors check only rendered when the intake had
received something. On engines below the support floor nothing ever
arrives, and that check is precisely the acceptance criterion there:
it now renders on both paths, and the closing note explains that red
collection rows plus a green cleanliness row is the expected shape.
The cookie was written percent-encoded. The modern bundle reads
document.cookie without decoding it, and an encoded value fails its
validation, so a session started here was discarded rather than shared
with a page that loads the standard bundle. Cookies already issued stay
readable: the read path keeps decoding.

The spec meant to catch this decoded the value before handing it to the
modern parser, so it validated a string that never exists in the browser
and passed on a cookie the modern bundle rejects. Removing the decode
makes it fail against the old implementation.

An untracked session is no longer treated as invalid either. The modern
bundle only mints an id once a session is tracked, so rum=0 with no id
is what a sampled-out session looks like, and renewing on a missing id
re-ran the sampling draw on a session that had already been sampled out.
Entries this build does not understand now survive a renewal too.
Four behaviours diverged from the standard bundle in ways that lose or
misreport data:

init overwrote a tracking consent the page had already set. A consent
management platform commonly answers before init runs, and the answer is
the user's; the configuration only supplies a default for a page that
has not answered.

The page exit guard was never released, so a cancelled navigation left
it set and the real exit that followed did nothing: everything recorded
after the cancellation went with the page. It is now released by the
next event, which only a page that is still recording produces.

View events were dated when the update was assembled rather than when
the view started, so the closing update appeared to have happened at the
moment the page was dismissed.

Navigation Timing is read off window rather than as a bare identifier.
Where the property is absent entirely a bare reference throws instead of
evaluating to undefined, and this runs inside the first view emitted
during init. The degraded environment suite now deletes the global
rather than defining it as undefined, which is the only form of the
hazard that reproduces it.
The loader chose the standard bundle whenever Promise and fetch were
present. Those are the two most commonly polyfilled APIs on the pages
this build targets, and a polyfill supplies the API without supplying
the syntax, so a polyfilled IE9 was handed a bundle it cannot parse and
collected nothing. document.documentMode is checked first: only Trident
defines it, it reports the mode the page is actually rendered in, and no
polyfill sets it.

The snippet also carried trailing commas, which an ES3 parser rejects
outright — in a snippet whose whole job is to route browsers that parse
that way. A build gate now parses every documented snippet as ES3, since
a formatter reinserts the comma given the chance.

The page load timings row is marked as uneven rather than supported: on
the IE9 device used for verification none were reported, while the same
code fills them in on a modern browser.
Running an install re-resolved the @alicloud subtree, which has nothing
to do with this branch: those packages are pinned to the floating latest
range, so any install moves them. Only the acorn descriptor is kept.
…ndle

startView ignores everything but the name, and a relative proxy resolves
against the document base url rather than the page url. Both are visible
to a page being ported and neither was written down.
The checks never looked at page load timings, which is why the IE9 run
passed every row while reporting none of them. The new row separates the
two explanations, which call for opposite responses: a browser without
usable Navigation Timing has nothing to report and says so, while one
that has it and still sends no timings is a bug here. The environment
box now shows what the browser actually offers, so a single run settles
which case it is.

The session cookie row also stops printing the whole cookie jar. This
page runs inside the customer's own environment and its results get
screenshotted; the rest of document.cookie belongs to whatever else is
served from that host.
Measured on a real IE9 rather than inferred from an earlier run that
showed none: window.performance and performance.timing are both present
there, and all five timings reach the intake. The defensive read stays,
because the engines below IE9 do not have the API at all.
The comment claimed the guard stops the synchronous request from being
sent twice, which is not what it does: an event arriving between
beforeunload and unload releases it and buys a second request. That is
the intended trade rather than a hole - the guard falls only when there
is new data an exit would have to carry, so the second request is what
delivers that event instead of losing it. Written down so the next
reader does not restore the one-shot guard, and the data loss with it.
stopSession tore the whole pipeline down, so a page that called it never
recorded again. It now ends the session and leaves collection running:
the next event opens a new session with a fresh sampling draw, which is
what the standard bundles do. The current view carries on, since there
is no session renewal signal here to hang a new one off.

init used `running` as its initialised flag, but nothing is running
between init and a consent grant, so a second init in that window
replaced the configuration the first one was waiting on and events went
to the wrong application. A separate flag closes the window; a rejected
configuration still leaves the SDK uninitialised so a corrected call
works.

The lockfile also lost this package's workspace entry when an unrelated
re-resolution was stripped out of it, which broke
`yarn workspace @flashcatcloud/browser-rum-legacy build:bundle` on a
clean checkout. Restored, without the unrelated churn.

The README said the package had never run on real hardware, three
paragraphs after describing what it did on real hardware.
setViewName started a fresh view, which invents a navigation the user
never made and splits the view's counts across two ids. An update has
already gone out under the old name and cannot be retracted, but every
update of a view shares its id and the intake keeps the highest document
version, so renaming in place lands the new name without the phantom
page view.
The state the modern bundle writes when a session ends is neither an id
nor a tracking decision: it is an expiry marker beside the anonymous
user id it deliberately carries across the expiry. This build required
one of its own fields to be present before it would accept a cookie at
all, so it threw that state away and reset an identifier the other build
was keeping. Any parsed entry is enough now.

The expiry marker is also a known field rather than a foreign one.
Carried forward as foreign it would have ridden into every session this
build writes, and the modern bundle reads any session carrying it as
expired — a new session on every page load. Fixing the parsing without
this would have been worse than leaving it broken.

Ending a session now writes that same expired state instead of deleting
the cookie, so what outlives a session survives it here too. Withdrawing
consent still deletes the cookie outright: there the identifier is the
thing being withdrawn.

Withdrawing consent also stopped sending. Tearing the pipeline down
buffers one last view update, and the buffer sends itself as soon as an
event would take it past its size limit, so a withdrawal with a nearly
full buffer put everything collected before it on the wire immediately
after. The batch is stopped first now, and the spec fills the buffer to
the edge rather than trusting a small one.
… does

What outlives a session stays in this cookie once the session ends - the
anonymous user id among them - but the cookie itself was written to
expire with the session, fifteen minutes out. A visitor coming back
twenty minutes later found it gone, which resets the identifier the
modern bundle keeps for a year and which the previous commit went to
some trouble to preserve.

The cookie now persists for that same year. How long a session lasts is
unchanged: that is decided by the expire entry inside the value, not by
the attribute.

The expiry attribute cannot be read back from document.cookie, so the
spec captures the write instead - which is why nothing caught this.
@Fiona2016
Fiona2016 merged commit ff7aee4 into main Aug 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant