Skip to content

feat(widget): add scoped better stack error tracking#550

Open
petar-omni wants to merge 1 commit into
mainfrom
feat/scoped-error-tracking
Open

feat(widget): add scoped better stack error tracking#550
petar-omni wants to merge 1 commit into
mainfrom
feat/scoped-error-tracking

Conversation

@petar-omni

@petar-omni petar-omni commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Resolves ENG-3743 and ENG-3744

Summary by CodeRabbit

  • New Features

    • Improved widget error handling with a clearer fallback dialog and a retry option.
    • Added support for reloading the app from the error screen.
    • Expanded error messages in English and French.
  • Bug Fixes

    • Widget errors are now captured more reliably, including rendering and route-related failures.
    • Retry actions now remount the widget to help recover from failed states.

@changeset-bot

changeset-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: dee8029

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@petar-omni petar-omni requested a review from dnehl June 26, 2026 23:10
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds Sentry-based widget error tracking, wires router and React root errors into that capture path, updates the provider tree, and expands the rich error modal with retry and reload text.

Changes

Widget error tracking and fallback UI

Layer / File(s) Summary
Sentry capture core
packages/widget/package.json, pnpm-workspace.yaml, packages/widget/src/providers/error-tracking/index.tsx
Adds the Sentry dependency and workspace catalog entry, then defines event scrubbing, lazy client setup, scoped capture, and widget-context updates.
Error boundary and sync exports
packages/widget/src/providers/error-tracking/index.tsx
Exports WidgetErrorDialog, WidgetErrorBoundary, ErrorTrackingProvider, and WidgetErrorTrackingContextSync.
Router and root error hooks
packages/widget/src/App.tsx
Adds route-level error handling, router remounting, and React root error callbacks that call captureWidgetException.
Provider tree rewire
packages/widget/src/providers/index.tsx
Removes the i18n and theme wrappers, reorders the top-level provider stack, and inserts WidgetErrorTrackingContextSync.
Rich error modal controls
packages/widget/src/components/molecules/rich-error-modal/index.tsx, packages/widget/src/hooks/use-rich-errors.ts, packages/widget/src/translation/*/translations.json
Exports RichError, extends the modal with controlled open/close/action/description props, and adds the new reload/error strings.

Sequence Diagram(s)

sequenceDiagram
  participant renderSKWidget
  participant ErrorTrackingProvider
  participant RouterProvider
  participant RouteErrorBoundary
  participant captureWidgetException
  participant BrowserClient
  participant WidgetErrorDialog

  renderSKWidget->>ErrorTrackingProvider: wrap the widget root
  ErrorTrackingProvider->>RouterProvider: render children
  RouterProvider->>RouteErrorBoundary: render errorElement
  RouteErrorBoundary->>captureWidgetException: report useRouteError() error
  captureWidgetException->>BrowserClient: captureException with scoped context
  RouteErrorBoundary->>WidgetErrorDialog: render retry dialog
  WidgetErrorDialog->>RouterProvider: onRetry remounts the router
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • dnehl
  • Philippoes

Poem

A bunny found a Sentry trail,
With retry hops that never fail.
If widgets wobble in the night,
A reload button sets things right. 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description only lists issue IDs and omits the required Added and Changed sections from the template. Add the required ## Added and ## Changed sections with a brief summary of the new scoped error tracking, error boundary, Sentry integration, and provider changes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: scoped error tracking for the widget.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/scoped-error-tracking

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

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​sentry/​react@​10.60.010010010096100

View full report

@aws-amplify-eu-central-1

Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-550.df4xyoi0xyeak.amplifyapp.com

@aws-amplify-eu-central-1

Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-550.d2ribjy8evqo6h.amplifyapp.com

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/widget/src/App.tsx`:
- Around line 72-83: Recreate the router on retry instead of only remounting
RouteErrorBoundary: the current routerVersion state is captured inside the
initial createMemoryRouter call in App, so resetRouter does not replace the
router or clear its error state. Update App so the router instance is rebuilt
when routerVersion changes, ensuring onRetry from RouteErrorBoundary triggers a
fresh memory router with cleared errors.

In `@packages/widget/src/providers/error-tracking/index.tsx`:
- Around line 44-45: The widget error context is currently stored in the
module-global latestWidgetContext, which lets one widget root overwrite
another’s wallet/route data. Move this context into the ErrorTrackingProvider
instance (or another per-root scope) and thread that scoped value through the
capture path in errorTrackingState so each widget root uses its own context.
Update the provider setup and the capture/use sites around latestWidgetContext
and ErrorTrackingState to read from the instance-scoped context instead of
shared module state.
- Around line 53-56: The redaction logic in redactedString only replaces the
word following authorization and can still leave a Bearer token visible in
values like Authorization: Bearer abc123. Update the redaction pattern in
redactedString to match and replace the entire Authorization header value,
including the Bearer scheme and token, while keeping the existing URL credential
redaction intact. Make sure the fix is applied in the same redaction pipeline so
any authorization string variant is fully masked.
- Around line 88-94: The scrubEvent helper currently redacts breadcrumbs,
contexts, extra, and request, but it still leaves sensitive data in the
top-level error fields. Update scrubEvent in error-tracking/index.tsx to also
scrub the message and exception payloads alongside the existing fields, using
scrubValue consistently so URLs, tokens, or payload fragments are removed before
the event is sent.
- Around line 144-162: Avoid sending raw wallet addresses to telemetry: in
error-tracking/index.tsx, the walletAddress is being added via setTag,
scope.setUser, and the wallet context in the same scope setup. Update this
scope-building logic to remove the direct walletAddress from all telemetry
fields, and if you still need correlation use a non-reversible redacted or
hashed identifier instead. Keep the rest of the context in
scope.setContext("stakekit_widget") and the wallet object, but ensure it no
longer contains the raw address.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3fe0dccf-30d7-4c82-ba70-5838ceac0ab9

📥 Commits

Reviewing files that changed from the base of the PR and between bc3a842 and dee8029.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • packages/widget/package.json
  • packages/widget/src/App.tsx
  • packages/widget/src/components/molecules/rich-error-modal/index.tsx
  • packages/widget/src/hooks/use-rich-errors.ts
  • packages/widget/src/providers/error-tracking/index.tsx
  • packages/widget/src/providers/index.tsx
  • packages/widget/src/translation/English/translations.json
  • packages/widget/src/translation/French/translations.json
  • pnpm-workspace.yaml

Comment on lines +72 to 83
const [routerVersion, resetRouter] = useReducer((state) => state + 1, 0);
const [router] = useState(() =>
createMemoryRouter([{ path: "*", Component: Root }])
createMemoryRouter([
{
path: "*",
Component: Root,
errorElement: (
<RouteErrorBoundary key={routerVersion} onRetry={resetRouter} />
),
},
])
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# If the repo includes lockfiles or dependency metadata, inspect the exact React Router version.
git ls-files | rg '(^|/)(pnpm-lock.yaml|package-lock.json|yarn.lock|bun.lockb|package.json)$'

Repository: stakekit/widget

Length of output: 390


Recreate the router when retrying route errors. routerVersion only affects the errorElement captured by the initial createMemoryRouter call, so resetRouter never updates the router instance or clears its stored error state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/widget/src/App.tsx` around lines 72 - 83, Recreate the router on
retry instead of only remounting RouteErrorBoundary: the current routerVersion
state is captured inside the initial createMemoryRouter call in App, so
resetRouter does not replace the router or clear its error state. Update App so
the router instance is rebuilt when routerVersion changes, ensuring onRetry from
RouteErrorBoundary triggers a fresh memory router with cleared errors.

Comment on lines +44 to +45
let errorTrackingState: ErrorTrackingState | null = null;
let latestWidgetContext: WidgetErrorContext = {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Scope widget error context per widget instance.

latestWidgetContext is module-global, so multiple widget roots on the same page can overwrite each other’s wallet/route context before capture. Store this context per provider/root instance and pass that scoped context into capture.

Also applies to: 211-213, 271-279

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/widget/src/providers/error-tracking/index.tsx` around lines 44 - 45,
The widget error context is currently stored in the module-global
latestWidgetContext, which lets one widget root overwrite another’s wallet/route
data. Move this context into the ErrorTrackingProvider instance (or another
per-root scope) and thread that scoped value through the capture path in
errorTrackingState so each widget root uses its own context. Update the provider
setup and the capture/use sites around latestWidgetContext and
ErrorTrackingState to read from the instance-scoped context instead of shared
module state.

Comment on lines +53 to +56
const redactedString = (value: string) =>
value
.replace(/(authorization)(["'`\s:=]+)([^"',\s}]+)/gi, `$1$2${redacted}`)
.replace(/(https?:\/\/)([^@\s]+)@/gi, `$1${redacted}@`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Redact the full Authorization: Bearer … value.

The current regex redacts Bearer but leaves the token in strings like Authorization: Bearer abc123.

Proposed fix
 const redactedString = (value: string) =>
   value
-    .replace(/(authorization)(["'`\s:=]+)([^"',\s}]+)/gi, `$1$2${redacted}`)
+    .replace(
+      /(["'`]?authorization["'`]?\s*[:=]\s*["'`]?)(?:bearer\s+)?[^"',\s}]+/gi,
+      `$1${redacted}`
+    )
     .replace(/(https?:\/\/)([^@\s]+)`@/gi`, `$1${redacted}@`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const redactedString = (value: string) =>
value
.replace(/(authorization)(["'`\s:=]+)([^"',\s}]+)/gi, `$1$2${redacted}`)
.replace(/(https?:\/\/)([^@\s]+)@/gi, `$1${redacted}@`);
const redactedString = (value: string) =>
value
.replace(
/(["'`]?authorization["'`]?\s*[:=]\s*["'`]?)(?:bearer\s+)?[^"',\s}]+/gi,
`$1${redacted}`
)
.replace(/(https?:\/\/)([^@\s]+)`@/gi`, `$1${redacted}@`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/widget/src/providers/error-tracking/index.tsx` around lines 53 - 56,
The redaction logic in redactedString only replaces the word following
authorization and can still leave a Bearer token visible in values like
Authorization: Bearer abc123. Update the redaction pattern in redactedString to
match and replace the entire Authorization header value, including the Bearer
scheme and token, while keeping the existing URL credential redaction intact.
Make sure the fix is applied in the same redaction pipeline so any authorization
string variant is fully masked.

Comment on lines +88 to +94
const scrubEvent = (event: ErrorEvent): ErrorEvent => ({
...event,
breadcrumbs: undefined,
contexts: scrubValue(event.contexts) as ErrorEvent["contexts"],
extra: scrubValue(event.extra) as ErrorEvent["extra"],
request: scrubValue(event.request) as ErrorEvent["request"],
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== file outline ==\n'
ast-grep outline packages/widget/src/providers/error-tracking/index.tsx --view expanded || true

printf '\n== relevant lines ==\n'
sed -n '1,220p' packages/widget/src/providers/error-tracking/index.tsx

printf '\n== search for redaction helpers and event processors ==\n'
rg -n "redactedString|scrubValue|scrubEvent|beforeSend|exception|message" packages/widget/src/providers/error-tracking packages/widget/src -g '!**/dist/**' -g '!**/build/**' || true

Repository: stakekit/widget

Length of output: 24401


Scrub message and exception too. contexts, extra, and request are redacted, but the top-level error message and exception payload can still carry URLs, auth tokens, or payload fragments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/widget/src/providers/error-tracking/index.tsx` around lines 88 - 94,
The scrubEvent helper currently redacts breadcrumbs, contexts, extra, and
request, but it still leaves sensitive data in the top-level error fields.
Update scrubEvent in error-tracking/index.tsx to also scrub the message and
exception payloads alongside the existing fields, using scrubValue consistently
so URLs, tokens, or payload fragments are removed before the event is sent.

Comment on lines +144 to +162
setTag(scope, "walletAddress", context.walletAddress);
setTag(scope, "wallet.network", context.network);
setTag(scope, "wallet.chainId", context.chainId);

if (context.walletAddress) {
scope.setUser({ id: context.walletAddress });
}

scope.setContext("stakekit_widget", {
...context,
captureMechanism: details.mechanism,
});

if (context.walletAddress || context.network || context.chainId) {
scope.setContext("wallet", {
address: context.walletAddress,
network: context.network,
chainId: context.chainId,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid sending raw wallet addresses to telemetry.

Line 144 tags the address, Line 149 sets it as the Sentry user id, and Lines 157-162 include it in context. That is a direct user identifier in third-party error tracking.

Proposed fix
-  setTag(scope, "walletAddress", context.walletAddress);
+  setTag(scope, "wallet.connected", Boolean(context.walletAddress));
   setTag(scope, "wallet.network", context.network);
   setTag(scope, "wallet.chainId", context.chainId);
-
-  if (context.walletAddress) {
-    scope.setUser({ id: context.walletAddress });
-  }
 
   scope.setContext("stakekit_widget", {
     ...context,
+    walletAddress: context.walletAddress ? redacted : undefined,
     captureMechanism: details.mechanism,
   });
 
   if (context.walletAddress || context.network || context.chainId) {
     scope.setContext("wallet", {
-      address: context.walletAddress,
+      address: context.walletAddress ? redacted : undefined,
       network: context.network,
       chainId: context.chainId,
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/widget/src/providers/error-tracking/index.tsx` around lines 144 -
162, Avoid sending raw wallet addresses to telemetry: in
error-tracking/index.tsx, the walletAddress is being added via setTag,
scope.setUser, and the wallet context in the same scope setup. Update this
scope-building logic to remove the direct walletAddress from all telemetry
fields, and if you still need correlation use a non-reversible redacted or
hashed identifier instead. Keep the rest of the context in
scope.setContext("stakekit_widget") and the wallet object, but ensure it no
longer contains the raw address.

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