Repository navigation
Interactions SDK 1.1.0: keep customer data out of test IDs - #6
Conversation
… hook, release browser and playwright 1.1.0
|
Starting initial review of |
There was a problem hiding this comment.
Summary
This PR adds a testId hook and a privacy-conservative defaultTestIdFilter. Every read of data-interaction-id and data-testid now goes through one helper, elementTestId. That covers the snapshot dictionary, click control_id, UI state events and, through them, finding stable_id and fingerprints.
Architecture
The shape is sound (KEEP_CURRENT). Replacing the three separate safeIdentifier(...) ?? safeIdentifier(...) read sites with one helper makes it hard for a new code path to skip the filter. A hook that throws or returns a non-string drops the ID silently. The result still has to pass the existing identifier shape check. The hook is part of the shared-instance identity, so two callers cannot silently share a runtime with different filters.
Checks
- The Playwright helper serializes the function and evaluates it in the page, with a fallback for method shorthand. A non-function result throws a clear error. This is documented as
page.evaluate-style self-containment. - Tests cover the default fixtures, a null-returning hook, a throwing hook with no console output, a non-string return, and a hook that replaces the default. There is also a Chromium e2e against the real
ui.findingevents. These guard real invariants. - The golden samples and protocol files change only because the dotted IDs became hyphenated. There is no schema change.
- The version bump and the new rule that a release bumps only the packages that changed are consistent across CHANGELOG, CONTRIBUTING and package.json.
One nonblocking suggestion is attached: the default filter is broad on digits, and an allowlist example in the README would help apps with static IDs like step2.
The PR is cohesive and does not need splitting.
Reviewed `b12f53b6`...`3640dcad`. Diff collection and GitHub posting were handled by the automation.|
Review head |
…, long tag paths and Playwright hook errors
|
Starting new changes review of |
There was a problem hiding this comment.
Summary
This follow-up (3640dcad...da5e309c) addresses the defects independent QA found on the first head. I re-read each change against the protocol schema and the surrounding code.
Architecture
KEEP_CURRENT. The single elementTestId helper remains the one place IDs are read. The new observedId cache is a small WeakMap keyed by the two attribute values, which stops a hook that writes aria-busy or disabled from re-queuing mutation records forever. It adds no new coordination machinery.
Checks
- Observer freeze: the hook now runs once per element and attribute-value pair. Changed IDs are still picked up, because the cache key includes both attributes.
- Async hook: attaching a no-op rejection handler to a thenable result stops an unhandled rejection reaching the page. The result is still dropped as a non-string.
- Tag path
control_id:slice(-128)matches the schema'smaxLength: 128(schema lines 808-811), and keeping the tail keeps the part nearest the control. - Older runtime: the identity length comparison closes the gap where
some()over a shorter array would match. The second caller gets an inactive handle, as documented. - Playwright helper: a
ReferenceErrorfrom the hook now fails the capture with a clear message, instead of every ID being silently dropped. Bound and native functions get an explicit error.defaultTestIdFilteris exposed on the page global for hooks to call. A hook that catches its ownReferenceErrorwould hide the failure, which is a narrow edge case. - Tests: the new tests exercise the behaviors fixed (observer termination, rejection, long path, older-runtime join, Chromium privacy, helper failure), and the PR description says they fail when each fix is reverted.
No actionable findings. Docs and CHANGELOG match the code.
Reviewed `3640dcad`...`da5e309c`. Diff collection and GitHub posting were handled by the automation.
QA re-verification: PR #6, quantfive/codepress-interactionsPR Head SHA:
DB PerformanceDB performance verifier skipped: no backend DB-backed performance surface. Inventory of the diff: Screenshot StatusNot applicable: the diff has no frontend or UI change. This is an SDK with no rendered interface, so no screenshots were captured. Overall verdict: FAILMuch narrower than last time. B1 to B10 and B12 pass; B10 was a FAIL on
There is also one decision for the owner: N1, a behaviour change introduced by the D2 fix. Status of D1 to D7
D1: what still drops every ID silentlyAll of these resolve with
D2: what still hangsEach case ran in its own browser and was killed by its own handle. Repro:
Cause: the memo keeps one key per element, so a changing ID always misses, and a new element has no entry at all. These are proven unbounded, not slow. With a cap added to the hook, the page ran 20,002 hook calls in 38 ms (alternate), 21,629 in 92 ms (spawn) and 20,002 in 79 ms (clone), and recovered only because of the cap ( All of these break the README's "free of side effects", so the trigger is narrower than the original D2, where a single constant attribute write was enough. A per-task limit on hook calls from the observer would close every shape at once, and could also replace the memo, which would remove N1. D5 matrix
In every case an inactive handle did not throw, shutting it down did not stop the first caller, and re-init after shutdown applied the right rule. Repro: New findingsN1 (medium, decision needed): the D2 memo can keep sending an ID the hook now dropsThe memo keys only on the two ID attributes. A hook whose answer depends on anything else keeps its first answer on state events, while clicks and snapshots use the current answer. This is new in
On the same page the click recorded the tag path and the snapshot dictionary was empty, so the three surfaces disagree. It fails toward sending, and the hook's The new README line "return the same answer for the same element" does cover this, so it can be accepted as a disclosed trade-off. It tells authors what to do but not what happens otherwise, and "on clicks and UI state changes" is now imprecise. The default filter is not affected: it depends only on the value, and 8 attribute changes tracked correctly with nothing dropped reaching the wire. Repro: N2 (low): the documented Playwright pattern does not type-check
N3 (low): a cut path with no slash is reported as non-structural
N4 (cosmetic): memo key collision on the literal value
|
| Row | Evidence class | Verdict | Observed | Artifact |
|---|---|---|---|---|
| B1 Default | LIVE, wire + memory | PASS | 26/26. domain-row-acme.com, row-3f2a…, item-42, acme absent from raw and gunzipped bytes; keep-all control sends all five |
artifacts/b1-results.jsonl, artifacts/b1/ |
| B2 Hook null | LIVE + packed /detect |
PASS | Part of 57/57 in b2to6; no IDs anywhere, snapshots differ, fingerprints identical |
artifacts/b2to6-results.jsonl, artifacts/b2/ |
| B3 Hook failure | LIVE, console + pageerror | PASS | 20 failing hooks, 0 console, 0 pageerror. Rejecting async hook now clean (D6) | artifacts/b2to6-results.jsonl, artifacts/attack-d6.jsonl |
| B4 Replace | LIVE, README block verbatim | PASS | order-row collapse, (v) => v keeps item-42 on all three surfaces |
artifacts/b4/ |
| B5 interaction-id | LIVE | PASS | All 7 cases | artifacts/b5/ |
| B6 Every read site | LIVE, raw bytes + keep-all | PASS, see N1 | Zero fixtures in default bytes; b6b 5/5. The two observer sites now go through observedId, which still calls elementTestId. N1 applies only to hooks that change their answer |
artifacts/b6/, artifacts/b6b-results.jsonl, artifacts/b6-read-site-grep.txt |
| B7 Consumers | LIVE + tsc --strict | PASS | tsc passes under bundler, nodenext, node10; wrong usages rejected; ESM and CJS OK; React StrictMode 4/4 with packed and published react | artifacts/b7/, artifacts/b7-react-results.jsonl |
| B8 Playwright helper | LIVE, scratch project | PASS, D1 partly fixed | Default and all hook styles honoured on all three helpers. CSP identical to origin/main below. Named edges reported above | artifacts/b8-observations.jsonl, artifacts/b8-attack.jsonl |
| B9 Artifacts | Tarball inspection | PASS | browser 1.1.0, playwright 1.1.0, react 1.0.0; VERSION and sdk_version 1.1.0; LICENSE, README, entry points present; workspace: rewritten to ^1.1.0; no env or credential files; npm publish --dry-run exit 0 for browser and playwright |
artifacts/b9/ |
| B10 Schema and regression | Commands + LIVE | PASS | Head payloads all valid. Schemas and generator unchanged. Goldens unchanged since 3640dca and differ from main only by IDs, version, timings and derived hashes |
artifacts/b10/ |
| B11 Docs truth | Executed claims | FAIL | Playwright README claim false for page-global names (D1). The rest verified, list below | artifacts/b8-attack.jsonl, artifacts/claims2-results.jsonl |
| B12 Overhead | LIVE bench, 5 rounds | PASS | Below | artifacts/b12-bench.log |
| Named edge cases | LIVE | FAIL | "Hook that mutates the element" fails (D2). All others pass: edges 47/47, ancestor 2/2, older runtime now passes |
artifacts/edges-results.jsonl, artifacts/attack-memo-hang.jsonl |
Checks across all probes: 198 pass, 9 fail, plus 201 observation rows. The 9 are D2 (7), N4 (1), N3 (1).
B10 counts
| Command | Result |
|---|---|
pnpm test |
83 passed |
pnpm typecheck |
exit 0 |
pnpm build |
exit 0 at both revisions |
pnpm test:browser, 3 runs |
each 71 passed, 1 skipped, plus 10 Playwright; no flake |
pack:check, 3 packages |
exit 0 |
npm publish --dry-run --ignore-scripts --access public, browser |
exit 0 |
uv run pytest -q |
13 passed |
uv run ruff check src tests |
exit 0 |
uv build |
exit 0 |
| CI on this head | all pass, publish jobs skipped |
Schema validation of every dumped payload: 85 batches, 100 snapshots, 1 invalid. That one is extra/longpath-main-none/batch-0.json, the deliberate origin/main control, which shows the original bug still exists at base. Head has 0 invalid, including all long-path and emoji-tag cases.
B11 claims
True when executed:
- The README allowlist block, verbatim, keeps exactly
save-button,nav:settings,step2. project-acme-launch,row-JaneDoeand a digit-free random ID are kept by the default;step2is dropped.- CHANGELOG: the tag path is cut to 128 keeping the end nearest the control; an older runtime is not joined; the second caller gets an inactive handle; a name missing from the page fails the capture.
claims.mjs9/9, covering the first run's table and CHANGELOG claims.
False: "using a variable from the test file fails the capture", for variables named after page globals.
B8 CSP comparison
| Policy | origin/main, no hook | head, no hook | head, hook |
|---|---|---|---|
script-src 'self' |
works | works | works |
default-src 'none' |
works | works | works |
| Trusted Types | works | works | works |
findIssues counts match at both revisions (3, 0, 3). No new limitation. The helper's new code runs inside page.evaluate, as before.
B12 numbers
One short rerun as agreed: 5 fresh-page runs per variant, interleaved, median of 5 timed walks after 3 warm-ups, 4,996 kept static IDs, ms. The runtime-path bench was not rerun. Other agent sessions were active on the machine.
| Variant | 1x CPU | 4x CPU |
|---|---|---|
| origin/main, kept IDs | 28.3 | 119.1 |
| head default, kept IDs | 28.5 | 123.6 |
| head, identity hook | 27.7 | 119.4 |
| head, hook with inline default rule | 28.6 | 121.7 |
| head, slow-ish hook | 32.2 | 138.5 |
| origin/main, no IDs (floor) | 27.0 | 115.4 |
| head default, all dropped | 28.5 | 122.2 |
The default adds 0.2 ms at 1x and 4.5 ms (about 3.8%) at 4x. The first run measured 2% at 4x with 15 runs. Five runs cannot show whether the .then check contributed, but neither figure is meaningful for a sliced background walk.
Limitations (not defects)
- Class hooks drop every ID silently; TypeScript rejects them.
- Cut paths can merge. Controls with different ancestors can cut to the same
control_id. Same family as the disclosed sibling merge. - Cut can split a surrogate pair with an emoji tag name over 128 units.
control_idthen starts with a lone surrogate. JSON and the schema accept it; a strict UTF-8 receiver might not. Not tested against one. - Older walker already in the page. If a 1.0.0
PAGE_SCRIPTis present, the helper reuses it, so hooks and the default are both ignored and all 6 IDs come back. This is D5's analogue in the helper, and needs two helper versions on one page. The result stays local to the test. - Rejections the SDK cannot see still surface, such as a floating
Promise.reject()inside the hook or one inside a returned array. Those are ordinary app bugs. - Inactive handles are silent;
getDiagnostics().enabledis the only signal. - The first run's limitations stand, and the README now names mixed-case and digit-free random IDs.
Gaps
qa/roundtrip.pynot run, as declared.- Firefox and WebKit not run.
- D4: not compared against the private repo, per the new rule.
- A real receiver's handling of a lone surrogate is untested.
Corrections to my own probes
No failure below came from the SDK:
- The first full pass exited 1 because zsh does not word-split env vars.
npm installpicked Playwright 1.65.0; pinned back to 1.64.1.- B4's README extraction took the new allowlist block first; now targets the
order-rowblock. parity.spec.tsclosed over the import, which now fails loudly as intended; rewritten to the README's written-out rule.attack-stale.mjsfirst countedstructural_changeevents, producing false FAILs; now filters tobusy.- My
ignoreattack assumed ignored elements skip the hook. They are walked and flagged, as at main. bench.mjsexited 1 after its last table becauseQA_ARTIFACTSwas unset, sob12-bench.jsonwas not written. The log has the numbers.- The first run's "writes hidden" hang label is corrected to
aria-expanded.
Hard rules
- No
pkill, and no pattern-based kill of any kind.hang.mjs's oldpkill -Pwas removed. - Every probe browser came from
launchServerwith its PID logged: 44 started, 44 killed by handle (artifacts/browser-pids.log). No recorded PID or process group remains. - Browsers started by the Playwright test runner (B8,
pnpm test:browser) are managed by that runner and were not logged individually. All runners exited and none hung. - Nothing read outside the checkouts and
$TMPDIR, apart from the Playwright browser cache that Chromium runs from, and GitHub for PR status.
Cleanup status
$TMPDIR/qa-pr6band$TMPDIR/qa-pr6b-mainremoved withgit worktree remove --force, thengit worktree prune. All exited 0; neither is ingit worktree list.- Author worktree: at
da5e309, clean, untouched by this run. - No process references this run. Repo-commands PID 52569 has exited.
- Kept: this run directory (143 MB), and the first run's directory.
probes/needsQA_REPOpointing at a fresh checkout; schemas used are inartifacts/protocol/.
…t caching its answer
|
Starting new changes review of |
There was a problem hiding this comment.
Summary
This follow-up (da5e309c...29be3946) replaces the observedId memo with a takeRecords() call at the end of the mutation observer callback whenever a custom hook ran.
Architecture
KEEP_CURRENT, and the change is a simplification. The memo was a second source of truth keyed on two attributes. It kept sending an ID after the hook began dropping it, which is the stale-answer problem QA raised. It also missed the changing-ID, spawn and clone hang shapes. The callback is synchronous, so any record pending at its end was written by the hook itself. Discarding those records breaks the feedback loop without caching any answers. The hook now runs on every state event, so a hook that starts dropping an ID takes effect immediately. The README line that required the same answer for the same element is correctly removed.
Checks
ranHookis set only when a custom hook exists. The default filter is pure and never triggers the discard, so there is no change for hook-free users.- Only records pending inside the synchronous callback are discarded, so page mutations are not lost.
- The README's "free of side effects" caveat is kept, and the PR description discloses the residual case of deferred writes (microtask or promise).
- The Chromium privacy and freeze tests and the new unit tests cover the behaviour. The PR description reports that removing
takeRecordsfails the freeze tests.
No actionable findings.
Reviewed `da5e309c`...`29be3946`. Diff collection and GitHub posting were handled by the automation.|
@codepress /judge-verification can you judge this verification? Final narrow QA: PR #6, quantfive/codepress-interactionsPR Head SHA:
DB PerformanceDB performance verifier skipped: no backend DB-backed performance surface. Inventory of the diff: Screenshot StatusNot applicable: the diff has no frontend or UI change. This is an SDK with no rendered interface, so no screenshots were captured. Verdict: PASS for this scopeAll five checks pass on the required evidence. Both previous failures and N1 are resolved, and no new defect was found.
Totals: 104 checks pass and 0 fail across the live probes, plus the 7 Playwright tests. That includes all of 1. D2Each case ran in its own browser, killed by its own handle. Hook count was read twice, 800 ms apart, after the capture finished. The queue was then drained, a real
The real click landed: The spawning hook is the one exception to the small counts. Negative control. The same probe against the 2. N1State events taken from the wire in HTTP mode, across allowed, denied and re-allowed phases.
In the first scenario Negative control. The same probe against the 3. B6Grep at this head: one attribute read of an ID ( 4. Normal pathsThe same page script ran against 5 variants: Real writes confirmed recorded after a hook-running callback:
GitHub checks at report time, recorded in 5. B11The sentence: "a name the page does not have fails the capture. A name the page does have, such as a test variable called
Status of earlier findings
Limitations
Corrections to my own probesThe first run showed failures with three causes. None came from the SDK, and each was resolved by evidence rather than by removing the check:
Before finalizing I also found that the first D2 run did not record whether the real click landed, and had no negative control. The D2 and N1 artifacts here come from the rerun that adds both. Rules followed
Cleanup status
|
|
👋 Hey @lightninglu10! Spawning a verification judge session to help with your request. View session |
|
Dispatched by judge-verification. Work the Path to merge below. Reuse healthy exact-head verification environments; do not redeploy or restart unless stale or unhealthy. Continue only when there is meaningful work to clear the blocker. If the path is already satisfied, the same blocker remains, or another judge pass would add no useful evidence, stop and report that instead of submitting another verification report. Verification Judgment — PR #6 (local)Verdict: ❌ NOT READY TO MERGE PR Head SHA: Summary: The five rows this report re-ran at the live head (D2, N1, B6, normal paths and repo commands, B11) are credible, and the fix in Revision binding
Diff trigger inventory
Per-item
Overall status check
Path to merge
Becomes Dispatch: yes What stood up
|
|
@codepress /judge-verification can you judge this verification? QA report: PR #6, quantfive/codepress-interactionsPR Head SHA:
DB PerformanceDB performance verifier skipped: no backend DB-backed performance surface. Inventory of the diff: Screenshot StatusNot applicable: the diff has no frontend or UI change. This is an SDK with no rendered interface, so no screenshots were captured. Overall verdict: PASSEvery row in the contract passes at this head, and there are no defects. How to read this report:
Per-row results
GitHub checks at report time: "Typecheck, unit and browser tests", "Test and pack" and "Python tests and lint" succeeded, and the npm publish jobs were skipped ( DefectsNone. Details by rowB1The page holds
B2
B3Each hook was wrapped in a counter and fired by mutations before any walk or click. All 19 hooks ran 6 times from the observer alone, then a real click, a snapshot and one more state write followed.
Control: an identity hook has the ID on all 7 state events. A further 20 failing hooks through the click and walk path pass in B4The README's replacement block was extracted from the checkout and bundled unchanged, apart from assigning the handle.
The indicator site gives the same answers: B5Expected and observed agree on 4 attribute events and 2 indicator events per case, plus the click and the snapshot dictionary.
B6One attribute read of an ID ( B7
B8
B9
B10All 75 batches and 81 snapshots in this run directory validate against the head schemas. Only B11The sentence: "a name the page does not have fails the capture. A name the page does have, such as a test variable called
B12Other agent sessions were active on the machine throughout: load averages ran from 3.8 to 8.6 ( Criterion. The contract sets no numeric budget, so I applied this: the default must add a negligible absolute cost per state change, and no variant may produce a callback long enough to affect the page. The default and identity hook meet it. The cost of a slower hook is the hook's own, and the README tells authors to keep it fast. Observer path.
1x CPU:
4x CPU:
Reading these:
One unexplained spike. On the cut burst the default rule costs 26.3 µs per callback against 18.1 at Runtime path. A snapshot through the real runtime and packed bundle, 4,996 kept IDs,
Walk. 4,996 kept IDs, 15 runs per variant, median of 5 timed walks after 3 warm-ups, ms (p25-p75):
The default adds 1.4 ms at 1x and 3.5 ms at 4x, about 3 to 5%. The identity hook shows none, which is consistent with this being the cost of the default's checks, though I did not profile it. Above, a 0.2 ms per ID hook stretches the walk to 1.1 s, but it stays sliced at a longest slice of 7.7 ms. D2Own browser per case, killed by its own handle. Hook count read twice, 800 ms apart, after the capture finished. The queue was then drained, a real
The real click landed: N1State events from the wire, across allowed, denied and re-allowed phases.
An element that stayed allowed kept its ID on all 12 state events, and clicks agreed across the same phases. Negative control: the same probe on Normal pathsThe same page script ran against 5 variants: Limitations and disclosed trade-offs
Gaps
Corrections to my own work
Rules followed
Cleanup status
|
|
👋 Hey @lightninglu10! Spawning a verification judge session to help with your request. View session |
|
Dispatched by judge-verification. This judgment is a VERIFICATION PASS at the current head. Run the guarded readiness transaction: Verification Judgment — PR #6 (local)Verdict: ✅ VERIFICATION PASS PR Head SHA: Summary: Every contract row was run fresh at the live head in real Chromium against tarballs packed from this head, with fixtures checked in raw wire bytes and keep-all controls so the searches can hit. Nothing is carried, so no manifest is needed. The earlier gaps (rows B1 to B5, B7 to B9, B12) are now covered, D2 and N1 carry negative controls against Revision binding
Diff trigger inventory
Per-item
Overall status check
What stood up
|
Built
testId?: (value: string, element: Element) => string | nulloption. It runs wherever a test ID or interaction ID is read: the snapshottest_iddictionary, a click'scontrol_id,control_idon UI state events, and so a finding'sstable_idand fingerprint.InteractionOptions. The browser package has noCaptureOptionstype; that name belongs to the Playwright helper, which gets the option too. The signature is exactly as specified.defaultTestIdFilter, exported and used when there is no hook. It drops any value containing a., and any value with a segment that has a digit or is longer than 32 characters. A hook replaces it.data-interaction-idfollows the same rule. When it is dropped, the element'sdata-testidis tried next, matching how an invaliddata-interaction-idalready behaved.routeTemplate.@codepress/interactions-playwright:testIdoncaptureSnapshot,findIssuesandexpectNoFindings. The function is serialized and runs in the page, so it must be self-contained, like apage.evaluatecallback. A hook that uses a name missing from the page fails the capture instead of silently dropping every ID, and it can callglobalThis.__codepressInteractions.defaultTestIdFilter. Its code changed, so it is bumped to 1.1.0.@codepress/interactions-reactpasses options through whole, so it needs no code change and stays at 1.0.0. Because that ends the "all three share one version" rule, CHANGELOG and CONTRIBUTING now say a release bumps only the packages that changed.click.error_after_clickfindings.control_idis cut to the protocol's 128 characters. The long path existed before, but dropping an ID made more controls hit it, and an invalid event can cost the whole batch.initInteractionsno longer joins a runtime from an older SDK copy on the page, which would ignore the hook.packages/browserbumped to 1.1.0 (VERSIONtoo), CHANGELOG entry added, and a "Test IDs and privacy" section added to the browser README. Root, protocol and Playwright READMEs updated.checkout.pay) became hyphenated (checkout-pay) because the default now drops dots. No schema change.private.indicator, which would otherwise have passed for the wrong reason in thedata-interactions-excludetest.Differs from the brief:
body/button,html/body/button, or#ancestor/div/button), and this PR keeps it. See the note under Could not verify.domain-row-acme.com, which has the same shape, because this repo's examples must stay generic.Verified
Run locally on the current code: browser build,
pnpm typecheck,pnpm test(87 passed) andpnpm test:browser(browser 74 passed and 1 skipped, the opt-in golden regeneration; Playwright helpers 10 passed).The first head was also checked independently against packed tarballs in real Chromium. That run and a rerun failed, finding the defects fixed above. See the QA reports on this PR for the result on the current head.
Default fixtures:
row-3f2a9c1e-…,domain-row-acme.comanditem-42are absent from the snapshot dictionary, clickcontrol_idand findingstable_id;save-buttonandnav:settingsare kept. Unit tests are intest/test-id.test.ts.test/e2e/privacy.spec.tsproves it in real Chromium against theui.findingevents the SDK actually sends.Negative, hook returns
nullfor everything: no test ID in any payload, andstable_idand fingerprints match across two different snapshots of the same elements (unit test).Negative, hook throws: capture continues, the ID is dropped, and
console.log/info/warn/error/debugare never called (unit test). A non-string return is also covered.Hook replaces default: a hook returning the value unchanged keeps
item-42(unit test, plus Chromium and the Playwright helper).npm publish --dry-run: passed locally and in the CIPublish dry runstep ofpublish-browser.ymlon the first head, printing+ @codepress/interactions-browser@1.1.0. It runs again on every push.Regression tests catch the fixes: reverting the rejection, tag path and older-runtime fixes one at a time made their tests fail. Removing
takeRecordsfailed all 6 Chromium freeze tests.Per-record split fixed: in Chromium the three data-bearing buttons now produce one finding instead of three.
Overhead (
pnpm --filter @codepress/interactions-browser bench, 5 runs, same machine, before → after). Body size falls because the bench page'scard-NIDs are now dropped, and that also lightens the walk, so the table is not a pure measure of the filter. On a page where every ID is kept, independent QA measured about 2% more walk time (27.1 → 27.7 ms at 1x CPU, 115.6 → 117.9 ms at 4x, 15 runs each), and a slow-ish hook costs about 20%:Could not verify
npm view @codepress/interactions-browser versionshowing 1.1.0: this can only be checked after merge.publish-playwright.ymlskips with a warning if it runs before browser 1.1.0 is on npm, and then needs a re-run. That is the documented process, but the order is not guaranteed.project-acme-launch), a name (row-JaneDoe) or a random ID with no digit is kept. The README says so and shows an allowlist hook.takeRecordsonly sees writes made during the callback. In Chromium a write fromqueueMicrotaskor a promise.thenstill freezes the page, and one fromsetTimeoutkeeps calling the hook without freezing it. Hooks must be free of side effects, and the README says so.status,name,location), a method that losesthis, a property of a missing page object, an async hook, and a generator hook. The README says to check the snapshot's IDs when writing a hook.globalThis.__codepressInteractionshas no global type, so the documented call givesTS7017in a strictly typed test file, and a cut tag path with no slash reportsstructural_id: false, which needs a control tag of 128 or more characters.