Skip to content

refactor(sdk): shared @maple/sdk-core for the browser and Effect SDKs - #1167

Merged
Makisuo merged 5 commits into
fix/browser-sdk-review-findingsfrom
feat/sdk-parity
Sep 30, 2026
Merged

Makisuo merged 5 commits into
fix/browser-sdk-review-findingsfrom
feat/sdk-parity

Conversation

@Makisuo

@Makisuo Makisuo commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #1168 (review fixes for the browser SDK); the diff is only this change.

What

First step of bringing @maple-dev/browser and @maple-dev/effect-sdk to parity from one implementation instead of two.

New private package @maple/sdk-core, bundled into each SDK the same way @maple/browser-session is:

  • Root, runtime-agnostic (safe for the Effect SDK's server and Workers presets):
    • per-session sampling decision with the W3C ot=th/rv tracestate
    • error filter factory. It was module state before, and two SDK copies can share a page
    • Caused by: rendering:
      • duck-typed, so it also walks Effect's pretty errors, which are plain Errors carrying copied cause/errors
      • it expands errors only on error-shaped values, and renders plain members (GraphQL-style { message }) by their message
      • it never throws on hostile objects (non-string stack, throwing getters)
    • HTTP status policy as pure predicates. The same METHOD url -> status|type message builder serves status errors and network failures
    • header allowlist (the same credential denylist as fix(browser): address review findings in the browser SDK and session replay #1168)
    • neutral SignalLogRecord, plus the shared option groups (tracing, errors, webVitals, breadcrumbs, logs, reporting, transport, replay) and their defaults
    • page-wide coordination under a versioned globalThis key (a copy with another layout gets its own slot and never wipes this one):
      • one reported-error set, so one error is one issue even with both SDKs loaded
      • an error-recorded bus, so breadcrumbs and buffered replay react to errors from either SDK
      • one lease per page option (breadcrumbs, console, csp, browserReports, longFrames, slowInteractions, webVitals), owned by the bundled copy. No option is collected twice, and each SDK keeps the options the other doesn't collect.
  • ./browser/* subpaths: web vitals, breadcrumbs, CSP/Reporting API, long frames and slow interactions, document timing, and the IndexedDB offline queue.
    • They emit through injected sinks (emitLog, startSpan, stash) instead of calling OTel.
    • Offline batches carry their endpoint and a credential fingerprint, so one SDK never resends another's batch under its key.
  • Architecture test:
    • forbids effect, @opentelemetry/* and anything that reaches rrweb (including @maple/browser-session/replay), in either quote style or require;
    • keeps the root tier free of DOM imports and DOM globals.

@maple-dev/browser now adapts these to OTel (sampler, status exporter, exceptionOf, deferred-chunk wiring) instead of owning them.

Why

The two SDKs had already drifted: the browser stack added bot exclusion, buffered replay, canvas and network bodies to one copy of the client session lifecycle and none to the other. Most stack features are export-time rules each SDK would otherwise reimplement.

Reviewer notes

  • Behaviour changes beyond the move:

    • window.onerror spans now carry code.column.number;
    • responseStatus accepts string status codes ("503");
    • an invalid replay.onErrorSampleRate now falls back to 0 instead of 1.
  • Offline batches are scoped to their endpoint and key. After a key rotation or an endpoint change, batches stored under the old credentials are no longer resent under the new ones; they expire after 24h. That is the price of never sending one org's data with another org's key.

  • The Effect SDK does not import sdk-core yet. Its devDependency is left out until the PR that does.

  • Size: the eager and first-party ceilings rise by 0.5 kB, with the reason in scripts/size.ts. The page-wide coordination accounts for most of it.

    • eager: 44.33 / 44.5 kB
    • first-party: 18.99 / 19 kB
    • deferred: 12.83 / 14 kB
  • Tests:

    • @maple/sdk-core: 45 tests
    • @maple-dev/browser: 124 tests
    • @maple/browser-session: 234 tests
    • @maple-dev/effect-sdk client: 37 tests
    • Typecheck is clean and both SDK builds pass.
  • Follow-ups (separate PRs): the Effect SDK side:

    • one client pipeline
    • URL scrubbing and a header allowlist on spans
    • filters, cause chains and the status policy in its encoder
    • session sampling
    • session.id/user.id/event names on logs
    • the deferred collectors
    • the replay option pass-through
    • the offline queue
    • a parity manifest and a size gate

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added shared configuration options for tracing, signals, replay, error filtering, and HTTP data capture.
    • Improved coordination between SDK copies on the same page, including collector ownership and reported-error tracking.
    • Refined offline data handling, sampling, browser signal collection, and error details.
  • Bug Fixes
    • Offline batches are now kept for the matching endpoint and authorization context and sent only when eligible.
    • Avoided duplicate browser collectors when multiple SDK copies are present.
  • Documentation
    • Updated bundle-size guidance and budgets.

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: b43e2be8-c2a3-440d-bb21-5a7db37bca59

📥 Commits

Reviewing files that changed from the base of the PR and between 1770f01 and 8adb3ce.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (47)
  • .oxlintrc.json
  • knip.json
  • packages/browser/package.json
  • packages/browser/scripts/size.ts
  • packages/browser/src/config.ts
  • packages/browser/src/deferred/index.ts
  • packages/browser/src/deferred/logs.ts
  • packages/browser/src/deferred/perf.browser.test.ts
  • packages/browser/src/error-causes.test.ts
  • packages/browser/src/error-causes.ts
  • packages/browser/src/error-filters.ts
  • packages/browser/src/errors.ts
  • packages/browser/src/http-headers.ts
  • packages/browser/src/http-status.ts
  • packages/browser/src/index.ts
  • packages/browser/src/logs.ts
  • packages/browser/src/navigation.ts
  • packages/browser/src/offline.browser.test.ts
  • packages/browser/src/sampling.test.ts
  • packages/browser/src/sampling.ts
  • packages/sdk-core/package.json
  • packages/sdk-core/src/architecture.test.ts
  • packages/sdk-core/src/browser/breadcrumbs.ts
  • packages/sdk-core/src/browser/document-timing.ts
  • packages/sdk-core/src/browser/offline.browser.test.ts
  • packages/sdk-core/src/browser/offline.ts
  • packages/sdk-core/src/browser/perf.ts
  • packages/sdk-core/src/browser/reports.browser.test.ts
  • packages/sdk-core/src/browser/reports.ts
  • packages/sdk-core/src/browser/web-vitals.ts
  • packages/sdk-core/src/error-filters.test.ts
  • packages/sdk-core/src/error-filters.ts
  • packages/sdk-core/src/errors.test.ts
  • packages/sdk-core/src/errors.ts
  • packages/sdk-core/src/http-headers.ts
  • packages/sdk-core/src/http-status.ts
  • packages/sdk-core/src/http.test.ts
  • packages/sdk-core/src/index.ts
  • packages/sdk-core/src/log-record.ts
  • packages/sdk-core/src/options.ts
  • packages/sdk-core/src/page.test.ts
  • packages/sdk-core/src/page.ts
  • packages/sdk-core/src/sampling.test.ts
  • packages/sdk-core/src/sampling.ts
  • packages/sdk-core/tsconfig.json
  • packages/sdk-core/vitest.config.ts
  • packages/tsconfig.browser.dts.json
 ______________________________________
< I read stack traces like tea leaves. >
 --------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Base automatically changed from feat/browser-sdk-landing-docs to feat/browser-sdk-long-frames September 29, 2026 22:51
@Makisuo Makisuo changed the title refactor(sdk): shared @maple/sdk-core for the browser and Effect SDKs (WIP) refactor(sdk): shared @maple/sdk-core for the browser and Effect SDKs Sep 30, 2026
@Makisuo
Makisuo changed the base branch from feat/browser-sdk-long-frames to fix/browser-sdk-review-findings September 30, 2026 09:19
@Makisuo
Makisuo marked this pull request as ready for review September 30, 2026 09:19
@maple-review-bot

maple-review-bot Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Maple review

🟡 Confidence 3/5 · needs attention
quality 100/100 · no findings · tests covered · risk medium · 2/2 new units observable

Warning

This review ended early; what follows is what it established.

Extracts a private @maple/sdk-core (runtime-agnostic root plus ./browser/* collectors that emit through injected sinks) and rewires @maple-dev/browser onto it, adding page-wide error de-duplication and per-collector leases. The moved logic matches the base line for line; safe to merge. The diff of packages/sdk-core/src/browser/reports.browser.test.ts was not read, so its assertions are unvouched for.

  • @maple/sdk-core owns the shared option groups and the single set of defaults both SDK configs extend
  • Page-wide state on globalThis: one reported-error WeakSet and a per-collector claimPageSignal lease
  • Browser collectors take injected sinks (emitLog, StartSpan, RecordChild) instead of calling OTel directly
  • Offline batches gain a target of endpoint plus credential fingerprint so one SDK does not send another's
What was checked
  • sampleSession/rejectionThreshold are byte-identical to the base packages/browser/src/sampling.ts; the ot=th/ot=rv values and the drop condition are unchanged
  • offline.ts drain still returns on the first 5xx/429, drops batches past 24h or predating a consent revoke, and trims to MAX_BATCHES (packages/sdk-core/src/browser/offline.ts:117)
  • CREDENTIAL_HEADERS (now including x-api-key) is still applied in resolveHeaderCapture before any header name is used (packages/sdk-core/src/http-headers.ts:23)
Observability coverage: 2 of 2 changes observable
Change Kind Observable Evidence
sdk-core browser collectors (web vitals, long frames/interactions, breadcrumbs, CSP reports) client-side span/log producers yes Emit through injected sinks (emitLog, StartSpan, RecordChild); packages/browser/src/deferred/index.ts:23 and :59 wire them to liveMapleTracer/emitLog
offline queue flush (POST <endpoint>/v1/<signal>) outbound HTTP yes Runs inside the browser SDK's own transport (packages/sdk-core/src/browser/offline.ts:127), not a Maple service; no span expected
Files not reviewed (1)

The review ended before it read these diffs, so nothing above vouches for them.

  • packages/sdk-core/src/browser/reports.browser.test.ts

2e63c20 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

…ct SDKs

One framework-free implementation of the browser signal behaviour both
published SDKs need, so they cannot drift.

@maple/sdk-core (private, bundled into each SDK like browser-session):
- root, runtime-agnostic: per-session sampling decision (ot=th/rv),
  error filter factory, Caused-by rendering that also walks Error-shaped
  copies (Effect's pretty errors), renders plain `errors` members by
  message and never throws on hostile objects, the HTTP status policy
  (status and network-failure messages), a header allowlist, the neutral
  log record, shared option groups and their defaults, and page-wide
  coordination under a versioned global: one reported-error set, an
  error-recorded bus, and a lease per page option owned by the bundled
  copy, so no option is collected twice and each SDK keeps its settings.
- ./browser/*: web vitals, breadcrumbs, CSP/reporting, jank, document
  timing and the IndexedDB offline queue, emitting through injected
  sinks. Offline batches carry their endpoint and a credential
  fingerprint, so one SDK never resends another's batch under its key.
- an architecture test forbids effect, @opentelemetry and anything that
  reaches rrweb, in either quote style, and keeps the root tier free of
  DOM imports and DOM globals.

@maple-dev/browser now adapts these to OTel instead of owning them. The
eager and first-party size ceilings rise by 0.5 kB for the page-wide
coordination, with the reason in scripts/size.ts.
- sdk-core joins browser and browser-session in the no-try-catch
  exemption: it ships inside them with no runtime dependencies, so
  try/catch is the only error handling it has.
- Attribute readers are typed (AttributeValue, ReadAttribute) instead of
  returning unknown, and the header filter takes and returns them.
- Console level severities are a Map, so no string reaches the prototype.
- page.test uses two small test seams instead of Reflect.get on globalThis.
- openNavigationSpan is gone: jank parenting uses navigationSpanAt.
- knip: web-vitals, like rrweb, is imported by a bundled private package
  and stays listed so the build keeps it external.
@maple-review-bot

maple-review-bot Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Maple review

Nothing to review

Warning

This review ended early; what follows is what it established.

Review could not be completed: every tool call (pr_changed_files, pr_context) failed against GitHub, so no diff was read. The change extracts a shared @maple/sdk-core package for the browser and Effect SDKs; it needs a human review of the new sampling, error-rendering, HTTP status policy and page-wide coordination code.

What was checked
  • Nothing: pr_changed_files and pr_context both returned errors for this pull request.

8adb3ce · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@Makisuo
Makisuo added this pull request to stack #1183 September 30, 2026 16:09
Comment thread packages/sdk-core/src/error-filters.ts Fixed
… into feat/sdk-parity

# Conflicts:
#	packages/browser/src/deferred/perf.browser.test.ts
#	packages/sdk-core/src/browser/offline.ts
@maple-review-bot

maple-review-bot Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Note

A newer push replaced fd4a9ea before its review finished. The latest commit is reviewed in a new comment.

CodeQL flagged the frame URL pattern as polynomial on page-controlled
input (a stack line of `at a://` followed by many `@a://`). Frames are now
parsed by position: take the location after the last `(`, `at ` or `@`,
strip `:line[:col]` from the end, then check the scheme with a regex
anchored at the start. Same results for V8, SpiderMonkey and
JavaScriptCore frames; a test pins a 250 kB hostile line under 200 ms.

The first-party size ceiling moves 19 -> 19.5 kB for the ~60 bytes the
parser costs over the regex.
@maple-review-bot

maple-review-bot Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 4/5 · likely safe to merge
quality 100/100 · no findings · tests covered · risk medium

Extracts @maple/sdk-core (sampling, error filtering/rendering, status policy, browser collectors, page-wide coordination) and adapts @maple-dev/browser to it. The delta since the last review (e780797) is the linear stack-frame parser, per-batch consent checks in the offline queue, and the extracted interactionKey; all are correct and tested.

  • frameUrl parses stack frames by position, replacing the backtracking FRAME_URL regex
  • Offline batches carry an endpoint+key target; drain checks hasConsent() per batch
  • interactionKey extracted, so events without interactionId are still spanned
  • startPerf emits through an injected StartSpan sink instead of reaching for OTel
What was checked
  • Ran the new frameUrl against the old regex under node on V8/Firefox/eval/port/anonymous frames: same result everywhere except uppercase AT , which no engine emits
  • offline.ts:107 keeps another target's batch only while unexpired and deletes (never sends) expired ones, so one key cannot resend another's data
  • interactionKey fallback (perf.ts:128) keeps 0/undefined semantics and the seen dedup unchanged

e780797 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@Makisuo
Makisuo merged commit 8687eb9 into main Sep 30, 2026
42 checks passed
@Makisuo
Makisuo deleted the feat/sdk-parity branch September 30, 2026 16:25
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.

2 participants