@codepress/interactions-browser

中文文档
1.1.0发布于 2026/11/06Apache-2.0TypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 3306

Privacy-first browser interaction evidence SDK: screen snapshots, click follow-through, UI checks and pluggable sinks

原始使用文档
文档目录11 节
  1. Test IDs and privacy
  2. Correlation
  3. Lifecycle and bounds
  4. Route templates
  5. Screen snapshots
  6. Click follow-through
  7. UI checks
  8. Measured behaviors worth knowing
  9. Performance signals
  10. Sinks
  11. Options

Browser interaction SDK

Initialize once at the application client entry point. Importing the package during SSR is safe; disabled/SSR initialization returns an inert handle.

import { initInteractions } from "@codepress/interactions-browser";

const interactions = initInteractions({
  projectKey: "public-project-key",
  endpoint: "https://interactions.example.com",
  allowedApiOrigins: [window.location.origin],
});

The endpoint is the service base URL; the SDK sends protocol v2 batches to /v2/batches and snapshot bodies to /v2/snapshots/{sha256} (schemas). Use sink instead of endpoint to send evidence anywhere else (see Sinks). The public key resolves project/environment on the service. environment is a local diagnostic configuration label and never grants authority. Service origin admission and lease configuration must already permit this installation. API origins default to this page's origin. Add a separate API origin explicitly only if it is controlled by your application and permits X-Interaction-Id in its CORS configuration.

Initial setup captures clicks on semantic controls (including dynamically mounted controls), form submissions, navigation signals, bounded structural DOM changes, and allowed-origin fetch/XHR observations. It does not require per-control annotations. Control identifiers use data-interaction-id/data-testid values that pass the test ID filter, otherwise an at-most-five-tag ancestry hint. This fallback is not unique and never scans sibling collections. These identifiers are structural hints and can change when layouts change. Mark private subtrees data-interactions-exclude. Do not put user data or secrets in explicit tracking identifiers.

No DOM text, HTML, input values, request/response bodies, URL paths, queries, cookies or user IDs are recorded. Mutation evidence reports changed-record counts, not a replay or an assertion that a spinner represents business completion. HTTP statuses are bounded data.phase labels such as http_201; browser request outcomes are completed/error, never server business outcomes. No durable browser storage is used.

Test IDs and privacy

A test ID is often built from data: row-${record.id}, domain-row-${hostname}. Recorded as is, it would carry that data off the page, and one per-record ID would split one finding into many. So every data-interaction-id and data-testid goes through a filter before it is used anywhere: the snapshot test_id dictionary, a click's control_id, control_id on UI state events, and a finding's stable_id and fingerprint.

The default filter, defaultTestIdFilter, keeps an ID only when it looks static. It drops a value that:

  • contains a ., or
  • has a segment (split on -, _, . or :) that contains a digit or is longer than 32 characters.
ValueDefault
save-button, nav:settings, plan_pro_ctakept
item-42, step2dropped: a digit
row-3f2a9c1e-7b4d-4e8a-9c21-5d6e7f809a1bdropped: a digit
domain-row-example.com, checkout.paydropped: a .

A dropped ID is treated as if the element had none. Clicks use the tag path, and findings use a structural path from the nearest ancestor with a kept ID. data-interaction-id follows the same rule as data-testid. When it is dropped, the element's data-testid is tried next.

The filter cannot recognize every value. A slug of short words (project-acme-launch), a name (row-JaneDoe) or a random ID with no digit looks static and is kept, and a static ID with a digit (step2) is dropped. If your IDs can hold slugs or names, or you need IDs the default drops, allow only the IDs you know:

const STATIC_TEST_IDS = new Set(["save-button", "nav:settings", "step2"]);

initInteractions({
  sink,
  testId: (value) => (STATIC_TEST_IDS.has(value) ? value : null),
});

The testId option replaces the default. It runs wherever the default would, with the attribute's value and the element. Return the value, or a static replacement, to keep it, and null to drop it:

import { defaultTestIdFilter, initInteractions } from "@codepress/interactions-browser";

initInteractions({
  sink,
  // Collapse per-record rows into one ID, and use the default for the rest.
  testId: (value) => (value.startsWith("order-row-") ? "order-row" : defaultTestIdFilter(value)),
});

The hook replaces the default rather than adding to it, so testId: (value) => value keeps item-42. Call defaultTestIdFilter(value) from the hook to keep its protection. The result must still be a plain identifier ([a-zA-Z][a-zA-Z0-9_.:-]*, at most 128 characters) or it is dropped. A hook that throws, or returns anything other than a string or null, drops the ID; capture continues and nothing is logged. The hook runs for each element with an ID that a snapshot walks, and on clicks and UI state changes, so keep it fast and free of side effects. Pass a stable function: like routeTemplate, it is compared by identity when initInteractions is called again, and a call with a different hook gets an inactive handle.

Correlation

Automatic clicks are separate interaction attempts. Automatic requests and mutation/navigation observations have unknown attribution: the SDK does not attach them to whichever click happened most recently. For known causal work use an optional explicit action:

await interactions.runAction("transfer.confirm", async (context) => {
  await context.fetch("/api/transfer", { method: "POST" });
  await someOtherWork();
  await context.fetch("/api/verify");
});

runAction sets context only during the callback's synchronous invocation. Ordinary fetch/XHR calls during that synchronous portion inherit the action. After await, a timer or another callback, use the supplied context.fetch to preserve the known link. Ordinary unbound fetch after an await and independent polling remain unlinked. This avoids global async context leaking between overlapping actions. Business callback return values, thrown errors, and rejected promises remain unchanged. Existing X-Trace-Id is preserved; raw headers are never recorded. Ingestion and unconfigured third-party origins are never instrumented or sent correlation headers.

Lifecycle and bounds

flush() attempts one immutable batch, returning without throwing transport errors. Retryable failures preserve the exact batch/event IDs and payload. Defaults: 1,000 queued events, 100 events/batch, 60,000 bytes/batch, flush every 5s, request timeout 10s, 3 retries with 1s exponential base, 50 mutation-observer work units per callback. Override using maxQueueEvents, maxBatchEvents, maxBatchBytes, flushIntervalMs, requestTimeoutMs, maxRetries, retryBaseMs, maxMutationRecords. maxMutationRecords retains its existing name but is one shared callback budget (maximum 1,000): records, ancestor privacy checks, and every inspected subtree node—including excluded or non-element nodes—consume units. It is never reset for another record or subtree. Excluded descendants are skipped, and exhaustion stops inspection; dropped diagnostics increment once for that truncated callback without claiming an exact count of unseen nodes or events. These are bounded locally. Overflow and exhausted retries increment dropped diagnostics; they never hold up business requests. Retry timers are driven by the flush interval. getDiagnostics() exposes local accepted/failed/dropped counts and errors; it does not claim end-to-end installation health.

Page exit attempts one small keepalive batch. Delivery is best effort and can fail, particularly when an upload is already active or the browser terminates. No secret is placed in a URL. HTTP 401/403 stops capture and clears pending evidence; reconnecting requires explicit reinitialization after fixing admission.

Repeated identical initialization shares one recorder through reference-counted leases. Each owner calls shutdown() on its handle; the last owner stops timers/observers, aborts SDK uploads, clears queues, and restores only patches still owned by the SDK. Later third-party patches are preserved. Different concurrent project configurations return an inert handle: shut down the old project before switching account/org so pending events are never relabeled. React Strict Mode may start a fresh recorder after a complete cleanup, which is safe.

Not covered: closed shadow roots, frame contents (frames are one opaque box), general Promise/timer propagation, persistent offline replay, input recording, screenshots, sampled completeness inference, or global cross-batch re-batching. Browser telemetry is untrusted evidence. Service v1 requires immutable batch retries; event IDs are never reassigned to another batch.

Structural feedback includes progress/status indicators appearing or disappearing, aria-busy transitions, and disabled controls. Values and status text are omitted; CSS-only spinners without semantic markup may remain an unknown structural change. Signed URLs are excluded from request instrumentation. Existing interaction headers are preserved; a valid UUID is used for correlation, otherwise attribution is unknown.

Route templates

Events and snapshots carry a route template, never the URL. By default every path segment is replaced by :id, so /u/jane-smith and /acme-corp/projects are both sent as /:id/:id: a lowercase slug can be a username, organization or title, so it is never kept unless the app says so. List the static words of your routes in routeSegments (/acme-corp/projects with routeSegments: ["projects"] becomes /:id/projects), or pass routeTemplate to return your router's own pattern. The query and hash are never used.

Screen snapshots

Each settled screen is recorded as element boxes in a columnar body (schema, example): parent, tag, role, safe test ID, x/y/width/height (viewport CSS pixels from getBoundingClientRect), z-index, CSS position, flags (visible, interactive, clipped, scrollable, overlay, ignored, pointer-events none, opaque, clips content, in shadow root, open popover, ellipsis or line clamp, broken image, aria-hidden on the element or an ancestor) and text length. "In shadow root" means the element's own tree is a shadow root; slotted light-DOM content is not flagged. The viewport records whether the primary pointer is coarse (touch) and the theme on screen (light, dark or unknown): the theme() option when given, else prefers-color-scheme. The ui.snapshot event carries the same theme, so screens can be grouped by route, viewport and theme without reading bodies. For elements with direct text it also records the box of the rendered text itself (from a Range, geometry only), so an element box can overlap something while its text does not. For visible interactive elements it probes elementFromPoint at the center and four inset points and stores how many land on something else and which element is on top. elementFromPoint is never called for other elements. No text, values, attribute values (other than role and safe test IDs), URLs or HTML are recorded.

Snapshots are taken when a route settles (including the first screen), after a click settles, after a resize settles and when the tab becomes visible again, never while it is hidden. handle.snapshot() takes one on demand. Each body is hashed (SHA-256 of the exact uncompressed bytes); a hash already sent in this page session is not sent again. With endpoint, the body is uploaded by HEAD then, only on 404, a gzip PUT to /v2/snapshots/{sha256}. The service must allow HEAD/PUT and the Authorization and Content-Encoding headers from your origin.

The walk runs in slices of idle time: the clock is checked every 8 elements and the walk yields once maxSliceMs has passed, so a slice overruns by at most one batch of 8. It stops at maxElements and marks the body truncated; a body larger than maxSnapshotBytes drops trailing rows and is also truncated. If the DOM, scroll position or viewport changes between slices the body is marked torn. Open shadow roots are traversed (slotted content under its slot); closed roots are one box. Canvas, iframes, embeds and video are one box flagged opaque. display: none subtrees are skipped. Content the browser does not render (the body of a closed <details>, content-visibility: hidden) is recorded without the visible flag. Subtrees matching ignoreSelector, [data-layout-ignore] or [data-interactions-exclude] are recorded as one box flagged ignored and never descended, so checks can skip them. Clip detection follows overflow and contain: paint; it does not model every case (for example absolutely positioned descendants escaping a non-positioned clipping ancestor).

Click follow-through

Every click on a control starts a follow-through window. It ends when the page has been quiet (no DOM change, no observed request in flight) for clickSettleMs, and is reported as one ui.click.settled event: time to first visible feedback (DOM change, route change, dialog or focus move), settle time, counts of mutations, requests, failed requests, uncaught errors and long tasks, layout shift score, whether the route changed, a dialog opened or focus moved, and the screen hashes before and after. Since 0.3.0 it also says whether the click landed on a control (target_interactive), changed a field's value (value_changed, for example a checkbox), put focus in a text field (field_focused; true again for each click in a field that is already focused, where the caret is the effect) or opened a new tab or window (new_context). Since 1.0.0 it says whether the click's own press, or the page while the click was open, changed the text selection or moved the caret (selection_changed); positions are compared, the selected text is never read. Focus and selection count only as answers for rage_click; they are not feedback for first_feedback_ms, dead_click or slow_feedback. Focus or feedback caused by a later pointer or key input is not credited to an earlier open click. Its interaction_id is the click's interaction.started ID and its attribution is always inferred; requests observed in the window are counted there and never attached to the click as explicit. A second click before the first settles closes the first as overlapped (both are flagged) and nothing is counted for both. A click still open after clickTimeoutMs, or when the page is hidden or unloaded, is reported incomplete. Since 0.3.0 a click's interaction.started also carries target_key, an opaque per-session number for the element clicked (only equality means anything), and a click on a <label> is recorded once, as a click on its field's control (for example a <button role="checkbox"> with a <label for>), not again when the browser forwards it to the field. A field's control is the nearest control above it, or the form it joins through the form attribute. Labels for disabled fields and links inside labels are clicks of their own.

UI checks

@codepress/interactions-browser/detect turns snapshots and events into findings. The checks are pure functions over data: no document, no window, the same results in the browser, in Node and in a test runner (Playwright helpers). By default the SDK runs them in idle time after each new snapshot, after each settled click and at each flush, and sends each finding as a ui.finding event once per fingerprint per page session (findings: false turns this off). The SDK remembers up to 2,000 sent fingerprints by recency: one found again counts as recent and is not resent, and past 2,000 the one gone longest without being found is forgotten, so it is sent again only if it comes back after that. A fingerprint found in the current run is never forgotten to make room, so a screen that keeps producing the same findings never resends them; if one run finds more than 2,000 new fingerprints, the rest wait for a later run. A click finding from before the first snapshot has no viewport yet, so it waits for the snapshot that gives it one (and with it the right width band and fingerprint); it is sent as it is only when the page is going away or the session takes no snapshots.

A finding is a pointer, not a verdict: it names the elements and carries evidence, measured facts only (numbers, booleans, or null for unknown). Whether it is a bug is for whoever reads it; checks do not filter by confidence.

Check (check)Reports
layout.overlapTwo elements whose visible boxes intersect by more than overlapMinPx in both directions, where neither contains the other. Subject is the element underneath. Evidence: overlap size, fraction of each covered, blocked hit-test points of controls underneath, whether the subject's rendered text is covered (null for opaque boxes), whether any of the subject's content (text, a control, an image or an opaque box) lies under the overlap (subject_content_covered), whether each element is a control (subject_interactive, other_interactive), CSS positions, depth to the shared parent, overlay flags.
layout.spillAn element crossing its nearest overflow: hidden/clip ancestor by more than tolerancePx. Only the outermost spilling element is reported.
layout.page_h_scrollThe document is wider than the viewport; subject is the element reaching furthest right.
layout.offscreen_controlA control outside the area the page can scroll to (or, if fixed, outside the viewport).
layout.small_tap_targetA control smaller than minTapTargetPx in either direction, only for touch input.
layout.clipped_textRendered text cut off by a clipping box, unless it ends in an ellipsis or line clamp.
click.dead_clickA settled click on a control with no feedback, request, route change, dialog, focus move, value change, field focus or new tab.
click.rage_clickrageClickCount clicks on the same element of one control within rageClickWindowMs, while some click went unanswered. Different fields of one form are not one burst, and a label click (which the browser forwards to its field) is one click. A burst where every settled click was answered is not rage: visible feedback (a stepper, a toggle), a caret in a focused text field (field_focused), or a changed text selection (selection_changed, for example a triple-click selecting a line). One still settling is decided on a later run. Evidence: click_count, span_ms, clicks_settled, clicks_with_feedback (answered clicks; null with no click follow-through).
click.slow_feedbackFirst feedback after slowFeedbackMs, or none while requests ran that long.
click.silent_failureA request failed during the click and the screen did not change.
click.error_after_clickAn uncaught error during the click.
stability.stuck_busyA busy or progress indicator still showing after stuckBusyMs.
stability.layout_shiftA performance window's layout shift score at or above layoutShiftScore.
stability.slow_inputSlowest event timing at or above slowInputMs.
stability.broken_imageAn image that finished loading with no image data.

What is not reported as an overlap or spill: dialogs, menus, listboxes, tooltips and open popovers against what is beneath them (they are separate layers, as is a fixed full-screen backdrop); content crossing the edge of a scroller (that is scrolling); content scrolled under a sticky or fixed element (once the page has scrolled; at the page origin a fixed or sticky element covering content is reported); subtrees with data-layout-ignore; elements of 1×1 px or less. A torn snapshot gives no position-based findings; rows missing from a truncated one give none. Click checks use only windows that settled: a click whose page navigated away, timed out or was overlapped by another click gives no dead_click.

Sorting puts blocked controls first (any *_blocked_points > 0), then covered or clipped text, then the rest, then decorative or empty findings. Inside the rest, findings that involve a control come first: a small tap target, an offscreen control, a click finding, an overlap or spill where an element is a control (*_interactive), or an outer pair that blocks a control inside its subject (subject_blocked_controls > 0, such as main × widget); involvesControl(finding) says which. Then by check, in order found. It never drops a finding. A finding is decorative or empty when one of its elements is a decorative layer (*_decorative: no text, no control, and pointer-events: none or aria-hidden), none of its elements holds text or a control (*_has_content: false, for example stacked or cropped images), or an overlap covers none of the subject's content (subject_content_covered: false). findingTier(finding) gives the tier, 0 to 3. The earlier tiers win: a decorative layer that covers text sorts with covered text. Overlap evidence also has subject_text_covered_ratio, the share of the subject's rendered text area that is covered.

Measured behaviors worth knowing

These are reported as measured. Each finding carries the evidence that says what it is, so a reader can decide without guessing.

  • A fixed widget next to <main> is reported against <main>. A chat button that is position: fixed and a sibling of <main>, sitting over a button inside it, is reported as main × chat, not save × chat. A collision repeated by nested elements collapses to its outermost pair, and that pair's evidence covers everything inside it. The covered button still shows up: subject_blocked_points and subject_blocked_controls count its blocked hit-test points, so the finding sorts first, and the subject's index points at <main>'s row, inside which the snapshot holds the button.
  • A fixed header over a padded <main> is reported, as empty. At the page origin, a position: fixed header over <main style="padding-top: 64px"> intersects <main>'s box, so it is reported as main × header. Its evidence says nothing is under it: subject_blocked_points: 0, subject_text_covered: false and subject_content_covered: false. It sorts with decorative and empty findings. If it is how your layout is meant to be, accept it with ignoreFingerprints in tests or put data-layout-ignore on the header. Once the page has scrolled, content under a fixed header is scrolling and is not reported.
  • A label click that the browser does not forward, and that changes nothing, is a dead click. A click on label content the browser does not forward to the field (a nested button, a link, the body of a <details>, an <img usemap> with an empty map), or on a label for a hidden or disabled field, is a click of its own. When nothing happens it gives dead_click on the label's control (for example the form), with target_interactive: true because a label counts as a control. A label for a disabled field gives the same.
  • A field joined to another form belongs to that form. A field with the form attribute, and any label for it, is recorded on the form it joins, wherever it sits in the DOM: one label click is one interaction.started on that form.

The fingerprint is 32 hex characters of SHA-256 over the check, the route template, the viewport width band and the elements' stable_id, never coordinates, scroll offsets or text. It is the same for the same bug at another scroll offset, in another session or on another machine. stable_id is the test ID or, when there is none or it was filtered out, a structural tag path from the nearest ancestor with a test ID (#cart/div/button); evidence subject_structural_id / structural_id says so. Structural paths change when the markup does, and siblings with the same tags share one.

import { checkEvents, checkSnapshot, formatFinding } from "@codepress/interactions-browser/detect";

const findings = [...checkSnapshot(body, { sha256 }), ...checkEvents(batch.events)];
findings.map(formatFinding);
Detect optionDefault
checksallShort names to run, e.g. ["overlap", "spill"].
overlapMinPx0.5Overlap must be wider and taller than this (sub-pixel rounding does not count).
tolerancePx1Spill, offscreen, clipped text and page scroll must cross by more than this.
minTapTargetPx24WCAG 2.2 target size minimum.
touchsnapshot's coarse pointerRun the tap target check.
viewportBands[480, 768, 1024, 1440]Width breakpoints for the fingerprint band.
slowFeedbackMs1000
rageClickCount / rageClickWindowMs3 / 1000
stuckBusyMs10000
layoutShiftScore0.1
slowInputMs200
nowlast event time (SDK: current time)When stuckBusyMs is measured.

Performance signals

Layout shifts, event timing (40 ms and slower), long tasks, image load errors and uncaught errors/unhandled rejections are counted and sent as perf.summary events. Each API is feature-detected: a signal the browser does not support is null and listed in unavailable, never reported as zero. Error messages, stacks, file names and image URLs are not read.

Sinks

sink: { sendBatch, sendSnapshot } replaces endpoint; projectKey is then not needed. Each method may return (or resolve) "accepted" (the default), "retry", "rejected" or "denied" (stops capture and clears pending evidence, like HTTP 401/403). A sink that throws, rejects or never settles cannot affect the page: calls are isolated, timed out after requestTimeoutMs and retried within the usual bounds. createMemorySink() keeps everything in memory for tests:

import { createMemorySink, initInteractions } from "@codepress/interactions-browser";

const sink = createMemorySink();
const handle = initInteractions({ sink });
await handle.snapshot();
await handle.flush();
sink.batches; // protocol v2 batches
sink.snapshots; // { sha256, serialized, body }

A sink that sends over the network gets context as its last argument: keepalive (the page is unloading; keep it small and use fetch's keepalive), signal (aborted at the timeout or when capture stops) and fetch, the page's fetch without the SDK's wrapper. Use context.fetch so the sink's own uploads are not recorded as the app's requests, which would also make every flush cause another. Requests a sink starts before its first await are skipped whichever fetch or XMLHttpRequest they use; later ones are recorded unless they use context.fetch. examples/react-vite has a sink that writes files through the Vite dev server.

const sink: InteractionSink = {
  async sendBatch(_batch, serialized, { keepalive, signal, fetch }) {
    const response = await fetch("/my-collector/batches", { method: "POST", body: serialized, keepalive, signal });
    return response.ok ? "accepted" : response.status >= 500 ? "retry" : "rejected";
  },
  async sendSnapshot({ sha256, serialized }, { signal, fetch }) {
    await fetch(`/my-collector/snapshots/${sha256}`, { method: "PUT", body: serialized, signal });
  },
};

Options

OptionDefaultNotes
endpoint + projectKeyHosted service base URL and public key.
sinkAlternative to endpoint.
releasenoneBounded release label sent with every batch.
routeSegmentsnoneStatic lowercase path words the default route template keeps, e.g. ["orders", "items"].
routeTemplate(url)every path segment :id unless in routeSegments; no query or hashReturn a bounded route label such as /orders/[id]. Values with a query or unsafe characters are dropped.
testId(value, element)defaultTestIdFilterWhich test IDs are recorded; see Test IDs and privacy.
requestLabel(url, init)noneBounded static label for an observed request ([A-Za-z0-9_.:/-], 64 characters).
propagateHeaderX-Interaction-IdCorrelation header name for allowed origins; false sends none. A readable X-Trace-Id response header is recorded.
snapshotstrueTurn screen snapshots off.
maxElements5000 (max 50,000)Elements per snapshot before truncated.
maxSliceMs6 (max 50)Main-thread time per walk slice.
maxSnapshotBytes1,500,000Uncompressed body cap.
maxSnapshotsPerSession100Walks per page session.
routeSettleMs1000Wait after a route change (and the first load) before a snapshot.
clickSettleMs500Quiet time that ends a click's follow-through.
clickTimeoutMs10000Longest follow-through before incomplete.
resizeSettleMs500Quiet time after the last resize.
visibleSettleMs300Delay after the tab is visible again.
ignoreSelectornoneExtra subtrees recorded as one ignored box.
findingstrueRun the UI checks and send ui.finding events.
detectdefaults aboveUI check thresholds.
themeprefers-color-scheme() => "light" | "dark" | "unknown": the theme actually showing, for apps with their own toggle. Read at every snapshot; any other value, or a throw, is recorded as unknown.

The transport options above (maxQueueEvents, flushIntervalMs, …) keep their v1 defaults.

版本与发布记录

@codepress/interactions-browser 共发布 4 个版本,首次发布于 2026/11/05;过去 12 个月发布了 4 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.1.0latest2026/11/064 天前
1.0.02026/11/054 天前
0.1.02026/11/054 天前
0.0.0-stage2026/11/054 天前

关于 @codepress/interactions-browser 的常见问题

@codepress/interactions-browser 是什么?

Privacy-first browser interaction evidence SDK: screen snapshots, click follow-through, UI checks and pluggable sinks

如何安装 @codepress/interactions-browser?

在项目目录运行 npm install @codepress/interactions-browser,或使用 pnpm add / yarn add / bun add。

@codepress/interactions-browser 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@codepress/interactions-browser 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@codepress/interactions-browser 可以商用吗?

@codepress/interactions-browser 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@codepress/interactions-browser 的下载量有多少?

根据 npm 官方下载统计,@codepress/interactions-browser 在 2026/10/10 – 2026/11/08 这 30 天内被下载 3,306 次,日均约 110 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@codepress/interactions-browser 还在维护吗?

最新版本 1.1.0 发布于 2026/11/06(4 天前);至今共发布 4 个版本,其中过去 12 个月发布了 4 个;首次发布于 2026/11/05。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。