feat(logs): capture console calls made before the logs script loads (#4036)
* Buffer console entries at init and replay them once remote config confirms capture Remote config's captureConsoleLogs verdict now persists to a hidden key. On the next visit that key does not start capture directly, since a cached true would have no stop path if remote config later says disabled. It only gates a small console recorder: entries buffer locally from init, replay through the console pipeline when remote config confirms capture, and get dropped with the console restored when it does not. Rebuilt on top of the core-pipeline refactor, replacing the pre-refactor commits on this branch. * Keep recording until the logs script initializes and hand it the raw buffer The entrypoint's stringifyArgsSafely now serializes replayed entries, so the core-side serializer is gone and the gap between remote config and script load no longer loses console calls. The recorder patches through the layered patch helper with __rrweb_original__ set, checks is_capturing before buffering, and drops the buffer after 30s if nothing settles. * fix(logs): buffer early console calls and replay them once capture is confirmed Also fixes `logs.captureConsoleLogs` passed to `init()` never enabling console autocapture: the extension is constructed before init applies user config, so the opt-in is now read in setup. * fix(logs): complete the remote kill switch for console capture Drop console records already queued when the server disables capture, and honour a persisted "disabled" verdict so the window does not re-open on every page load until remote config speaks again. * fix(replay): let either patch copy splice past the other's wrapper rrweb ships its own copy of `patch` that walks only `__rrweb_layer__`, so stopping session recording while the logs recorder sat above it left the rrweb wrapper in the chain. Publish the layer under both marker names. * fix(logs): fall back to the persisted verdict when no fresh one arrives A failed remote-config fetch now behaves like a response without a logs key, matching session recording. Teach rrweb's patch copy to walk posthog layers so already-released clients get the cross-library teardown fix too. * revert(logs): let a local captureConsoleLogs opt-in stand The server reports captureConsoleLogs false for every project that has not turned it on, so it cannot tell "not enabled" from "turned off" and must not revoke an opt-in the caller made in init. Matches the logs spec, which states there is no remote kill switch for logs. The remote flag still withdraws the persisted hint, which is the only thing it granted. * test(logs): pin the invariants the kill-switch revert left uncovered A persisted "disabled" must not start a recorder, the hint teardown must drop its buffer, and rrweb's own suite now fails if its walk stops recognising posthog layers. Adds @posthog/rrweb-plugin-console-record to the changeset — it inlines the patched helper. * fix(logs): drop console records already captured when the user opts out Opting out released the pre-load buffer but left records the logs script had already mirrored to flush. Programmatic captureLog and logger keep their own queue, which consent gates separately. * docs(logs): correct stale comments and drop redundant ones The rrweb walk comment described the opposite of what it does, the entrypoint had stopped deriving its console levels from the shared tuple, and several comments restated the line beneath them. Also drops a patch test that killed no mutant its neighbour did not. * fix(logs): stop an in-flight batch discarding records captured after a clear Clearing the console queue on opt-out reset the bookkeeping that tells the queue advance how much was already accounted for, so a batch awaiting its response sliced off records captured after a later opt-in. Core now credits the cleared count the way a FIFO eviction does. * chore(changeset): merge the two @posthog/core entries * fix(logs): apply the in-flight credit to reset() as well as opt-out reset() runs on every identity change, and it cleared the logs queues the same unaccounted way opt-out used to. Core no longer zeroes the credit a batch in flight still needs; the flush loop already zeroes it per batch. * chore(logs): share the console marker walk and declare the rrweb-utils dep The lifecycle rebuild reintroduced a second copy of the marker walk in the logs entrypoint, so the shared module's docstring was claiming a guarantee it did not have. Three tests imported @posthog/rrweb-utils through a jest alias only, which typed the layer contract they exist to pin as `any`. * fix(logs): record console during the first remote-enable load and retire flushes across clearQueue Start the pre-load recorder when remote config is the first thing to enable capture, and give the logs queue a generation so a batch in flight cannot re-send or advance past records a clearQueue purged. * fix(logs): scope the queue generation to each batch and keep a hint-started recorder withdrawable Capturing the generation once per flush retired a batch assembled after a mid-flush clear, leaving it sent but never advanced. Starting a recorder no longer adopts one that setup() started from the persisted hint. * fix(logs): keep the early console buffer once remote config has granted capture A second remote-config result — session recording requests one when its persisted copy is stale — withdrew a hint-started recorder even though the grant had already happened and the handover was in flight, dropping the early lines it exists to preserve. Withdrawal now applies only while nothing has granted capture, which makes _recorderStartedByHintOnly redundant. * chore(changeset): drop rrweb packages changesets bumps automatically Only @posthog/rrweb-utils and posthog-js changed; the other rrweb packages pick up a dependency bump via updateInternalDependencies. * chore(logs): tighten comments and drop test casts * fix(logs): record the new private names and stop probing a console marker in e2e The pre-load recorder marks its console wrapper the way the lazy bundle does, so __rrweb_original__ no longer tells the two apart. Spy on captureConsoleLog and captureBufferedConsoleLog and poll for the record instead, which holds whether the call goes live or through the buffer. --------- Co-authored-by: Anna Garcia <anna.g@posthog.com>
K
Kimaswa Emmanuel Yusufu committed
718beee86aa31026beef9af2c13b049e9b847721
Parent: 712223a
Committed by GitHub <noreply@github.com>
on 8/27/2026, 11:47:02 AM