Skip to content

refactor: anonymized diagnostic-snippet telemetry + embed the deslop engine in @react-doctor/core#912

Open
aidenybai wants to merge 14 commits into
mainfrom
scrambler
Open

refactor: anonymized diagnostic-snippet telemetry + embed the deslop engine in @react-doctor/core#912
aidenybai wants to merge 14 commits into
mainfrom
scrambler

Conversation

@aidenybai

@aidenybai aidenybai commented Jun 20, 2026

Copy link
Copy Markdown
Member

Why

Two related strands of work on this branch:

  1. Anonymized diagnostic snippets for telemetry. We want structural insight into which code shapes trigger diagnostics without ever shipping user source. scramble is an AST-based anonymizer (oxc) that blinds identifiers, literals, and JSX/text while preserving structure and producing a stable FNV-1a fingerprint, so we can ship a scrambled snippet around each diagnostic to Sentry.

  2. One source of truth for the deslop engine. The deslop dead-code/duplication engine lived in the published deslop-js package, but @react-doctor/core is the diagnostic engine the whole monorepo builds on. This embeds deslop into core and makes deslop-js/deslop-cli thin facades, so everyone consumes core and there's a single engine to maintain — mirroring how react-doctor already houses its internals in core.

What changed

Scramble + telemetry

  • Added scramble, the AST-based snippet anonymizer, and housed it as an internal react-doctor util (scramble-snippet.ts) rather than a deslop-js public API.
  • New record-diagnostic-snippets.ts: converts oxlint UTF-8 byte offsets → UTF-16, scrambles the minimal AST node at each diagnostic, dedupes by structural hash, caps per scan, and emits one Sentry span per distinct snippet. Wired into inspect.ts.
  • Extracted the shared utf8OffsetToUtf16 helper into core and reused it.
  • Deduped OxcAstNode/isOxcAstNode onto deslop-js's canonical definition.

Embed the deslop engine in core

  • Moved the entire engine deslop-js/src/**@react-doctor/core src/deslop/**, exposed via the @react-doctor/core/deslop subpath. deslop-js/src/index.ts is now export * from "@react-doctor/core/deslop" — public API (analyze, defineConfig, isOxcAstNode, all types) unchanged.
  • vp pack bundles the engine into deslop-js's dist (CJS + ESM) with a sibling parse-worker.mjs, keeping oxc/glob deps external. core emits its own dist/deslop.js + dist/parse-worker.mjs.
  • Broke the turbo build cycle: core no longer declares deslop-js (it only resolves the "deslop-js" specifier at runtime via import.meta.resolve); react-doctor, deslop-cli, and api each depend on deslop-js directly.
  • core's dead-code integration tests use a new in-process worker (in-process-dead-code-worker.ts) that mirrors the production worker's normalization, so they test core's own engine without depending on the not-yet-built facade.
  • Moved the deslop test suite + fixtures into packages/core/tests/deslop and switched node:testvite-plus/test; scoped vitest to skip the broken-by-design fixture files.

Changesets: scramble-diagnostic-snippets (react-doctor), deslop-export-oxc-ast-node + deslop-engine-in-core (deslop-js).

Test plan

  • pnpm build — turbo graph acyclic; core emits deslop.js + parse-worker.mjs; facade emits dual CJS/ESM + its own parse-worker.mjs
  • pnpm typecheck, pnpm lint, pnpm format:check — green
  • pnpm test — 11/11 packages (core incl. the 506 migrated deslop tests; deslop-cli against the facade; react-doctor e2e)
  • pnpm smoke:json-report — OK
  • Verified bundle purity: oxc-parser/oxc-resolver/fast-glob/minimatch/typescript stay external in both core's deslop.js and the facade's index.cjs/index.mjs
  • End-to-end: built CLI scan of a temp project reports unused-file + unused-dependency (facade → bundled engine → spawned parse-worker.mjs)

Note

Medium Risk
Large monorepo packaging refactor (engine ownership, build graph, published bundles) plus new telemetry that touches source files; snippet content is structurally anonymized and gated on Sentry tracing, but any telemetry path warrants careful privacy review.

Overview
Moves the deslop dead-code engine into @react-doctor/core (src/deslop, @react-doctor/core/deslop export) and makes deslop-js a pure re-export while vp pack still bundles CJS/ESM plus parse-worker.mjs so the published package stays self-contained. @react-doctor/core drops its workspace dependency on deslop-js and pulls in the former engine deps (oxc-parser, fast-glob, etc.); api and language-server gain a direct deslop-js workspace link to keep the turbo graph acyclic. The deslop test suite and fixtures relocate to packages/core/tests/deslop, with dead-code integration tests using an in-process worker instead of spawning the facade.

Adds optional Sentry telemetry for structural diagnostic shapes: new scramble (oxc AST anonymizer) and recordDiagnosticSnippets, which convert oxlint UTF-8 byte offsets to UTF-16, scramble the minimal node around each diagnostic, dedupe by hash, cap samples, and emit child spans when tracing is on—wired from inspect.ts. Shared utf8OffsetToUtf16 is extracted in core and reused by oxlint binding resolution. isOxcAstNode / OxcAstNode are published from the deslop entry for AST walking.

Reviewed by Cursor Bugbot for commit 596c0c7. Bugbot is set up for automated code reviews on this repo. Configure here.

@pkg-pr-new

pkg-pr-new Bot commented Jun 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/eslint-plugin-react-doctor@912
npm i https://pkg.pr.new/oxlint-plugin-react-doctor@912
npm i https://pkg.pr.new/react-doctor@912

commit: 596c0c7

Comment thread packages/deslop-js/src/normalize-snippet/normalize-code-snippet.ts Outdated
aidenybai added a commit that referenced this pull request Jun 20, 2026
When `language` is omitted, scramble defaulted to tsx, so plain-TS
snippets with value-position generics (`fn<T>()`, `<T,>() => …`) parsed
under JSX rules and returned null. Respect an explicit language; with no
hint, try tsx then fall back to ts. Addresses Bugbot review on #912.
Comment thread packages/react-doctor/src/cli/utils/scramble-snippet.ts
Comment thread packages/react-doctor/src/cli/utils/scramble-snippet.ts
@aidenybai aidenybai changed the title feat(deslop-js): add scramble, an AST-based snippet anonymizer feat(deslop-js): ast Jun 20, 2026
Rewrites a snippet so every identifier (incl. React APIs, JSX tags, and
DOM/a11y attributes) becomes a role-prefixed placeholder applied
consistently so aliasing survives, and every literal is blinded. Returns
the readable scrambled source plus a stable FNV-1a hash. Parsing respects
an explicit `language`; with none it tries tsx then falls back to ts so
value-position generics still parse.
@aidenybai aidenybai changed the title feat(deslop-js): ast feat(deslop-js): add scramble, an AST-based snippet anonymizer Jun 20, 2026
Comment thread packages/react-doctor/src/cli/utils/scramble-snippet.ts
- Keep the leading `#` on PrivateIdentifier placeholders (and scope their
  lookup key) so private fields stay private, re-parse, and never collide
  with a public name of the same text.
- Blind JSXText runs so visible text between tags isn't leaked.
- Visit an identifier's children so a typed binding's `typeAnnotation`
  names are scrambled too.
Comment thread packages/react-doctor/src/cli/utils/scramble-snippet.ts
A single source name can play two roles (e.g. `className` as both a
destructured var and a JSX attribute label). Keying the placeholder cache
by name alone let the first-seen role win, so structurally identical
snippets that differed only in an underlying name scrambled (and hashed)
differently, breaking the naming-invariant dedup key. Key by (role, name).
Comment thread packages/deslop-js/src/normalize-snippet/normalize-code-snippet.ts Outdated
Blanking a TemplateElement used `raw.length`, which can exceed the
reported span when the quasi contains escapes — overrunning `span.end` and
rewriting unrelated source. Trim the actual delimiter chars off the span
ends instead, keeping the blanked region strictly inside the node and
escape-safe in both parser modes.
scramble is a react-doctor-specific snippet anonymizer (for telemetry /
privacy), not a general-purpose deslop-js capability. Exposing it on the
separately-published deslop-js package needlessly widened that package's
surface. Relocate it to react-doctor as an internal cli/utils helper
alongside the other anonymizers, drop the now-unneeded deslop-js export +
changeset, and add oxc-parser as a direct react-doctor dependency.

Implementation simplified while preserving identical output/hash: a single
local AstNode/Span type, inlined isAstNode guard, no offsetShift round-trip
in the template branch, and trimmed comments.
@aidenybai aidenybai changed the title feat(deslop-js): add scramble, an AST-based snippet anonymizer refactor(react-doctor): house scramble snippet anonymizer as an internal util Jun 22, 2026
Comment thread packages/react-doctor/src/cli/utils/scramble-snippet.ts
…ot bytes

oxc AST spans (and String.slice) are UTF-16 code-unit indices, so the
diagnostic offset/length must be too. The prior "byte range" wording invited
a caller to pass raw oxlint Diagnostic byte offsets, which would pick the
wrong node on non-ASCII source. Document the actual UTF-16 contract.
Drop narration comments that restate the code; keep only the notes that
guard real footguns the code can't convey — the UTF-16 offset contract,
the tsx→ts parse fallback, oxc's template-span quirk, and the role-keying
hash-invariance rationale.
Rename the local AstNode interface (and its isAstNode guard) to OxcAstNode /
isOxcAstNode, matching the existing names in deslop-js's oxc-ast-node.ts, and
align the field types (start?/end? as number) with that canonical shape.
scramble redeclared OxcAstNode/isOxcAstNode, a near-copy of deslop-js's
oxc-ast-node util. Expose that guard + interface from deslop-js's entry
(they were already internal) and import them in react-doctor instead of
keeping a third copy. No runtime behavior change.
For each scan, scramble a capped, hash-deduped sample of the structural
shapes rules fire on (identifiers/literals blinded) and emit them as
child spans of the run trace. No real source, names, or paths leave the
machine; a no-op when Sentry tracing is off.

Promotes core's private getUtf16Offset to a shared utf8OffsetToUtf16
util (reused by resolve-use-call-binding) for the byte->UTF-16 offset
conversion the scramble call needs.
…i become facades

Move the deslop analysis engine into the private @react-doctor/core package
(under src/deslop, exposed via the @react-doctor/core/deslop subpath) so the
whole monorepo consumes one source of truth, and reduce deslop-js to a thin
re-export facade over it. vp pack still bundles the engine into deslop-js's
published dist (CJS + ESM) alongside the sibling parse-worker.mjs, keeping the
tarball self-contained; the public API (analyze, defineConfig, isOxcAstNode,
every exported type) is unchanged.

To keep the turbo build graph acyclic, core no longer declares deslop-js (it
only resolves the "deslop-js" specifier at runtime via import.meta.resolve);
the packages that run the dead-code path (react-doctor, deslop-cli, api) each
depend on deslop-js directly. core's dead-code integration tests now use an
in-process worker so they exercise core's own engine instead of the
not-yet-built facade. The deslop test suite + fixtures moved into
packages/core/tests/deslop and switched from node:test to vite-plus/test.
@aidenybai aidenybai changed the title refactor(react-doctor): house scramble snippet anonymizer as an internal util refactor: anonymized diagnostic-snippet telemetry + embed the deslop engine in @react-doctor/core Jun 23, 2026
Comment thread packages/react-doctor/src/cli/utils/record-diagnostic-snippets.ts
# Conflicts:
#	packages/core/tests/deslop/fixtures/dependency-tooling/.gitignore
#	packages/core/tests/deslop/fixtures/dependency-tooling/node_modules/@babel/cli/package.json
#	packages/core/tests/deslop/fixtures/dependency-tooling/node_modules/@formatjs/cli/package.json
#	packages/core/tests/deslop/fixtures/dependency-tooling/node_modules/@tauri-apps/cli/package.json
#	packages/core/tests/deslop/fixtures/dependency-tooling/node_modules/@tinacms/cli/package.json
#	packages/core/tests/deslop/fixtures/dependency-tooling/node_modules/chokidar-cli/package.json
#	packages/core/tests/deslop/fixtures/dependency-tooling/node_modules/jest-cli/package.json
#	packages/core/tests/deslop/fixtures/dependency-tooling/node_modules/react-chartjs-2/package.json
#	packages/core/tests/deslop/fixtures/dependency-tooling/node_modules/react-redux/package.json
#	packages/core/tests/deslop/fixtures/remark-config-deps/.gitignore
#	packages/core/tests/deslop/fixtures/remark-config-deps/node_modules/remark-cli/package.json
#	packages/deslop-js/src/index.ts
analyze.test.ts asserts POSIX-relative paths against the deslop engine's raw output, which uses OS-native separators (production normalizes via check-dead-code's toRelativeFilePath). The cases never executed on Windows under deslop-js's `node --test tests/*.test.ts` glob; exclude them on win32, mirroring check-dead-code.test.ts's existing skipIf(win32).

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c674127. Configure here.

Comment thread packages/core/package.json
scanWorkspaceFull enqueues scans with runDeadCode: true, which routes
through core's checkDeadCode and resolves deslop-js via
import.meta.resolve("deslop-js"). Since core no longer depends on
deslop-js (it resolves the specifier at runtime), each consumer that
triggers dead-code must declare it directly — react-doctor, deslop-cli,
and api already do; this adds the missing dependency to language-server
so full workspace audits resolve the engine under pnpm's strict layout.
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