feat(widget): add scoped better stack error tracking#550
Conversation
|
📝 WalkthroughWalkthroughThis 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. ChangesWidget error tracking and fallback UI
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
This pull request is automatically being deployed by Amplify Hosting (learn more). |
|
This pull request is automatically being deployed by Amplify Hosting (learn more). |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (9)
packages/widget/package.jsonpackages/widget/src/App.tsxpackages/widget/src/components/molecules/rich-error-modal/index.tsxpackages/widget/src/hooks/use-rich-errors.tspackages/widget/src/providers/error-tracking/index.tsxpackages/widget/src/providers/index.tsxpackages/widget/src/translation/English/translations.jsonpackages/widget/src/translation/French/translations.jsonpnpm-workspace.yaml
| 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} /> | ||
| ), | ||
| }, | ||
| ]) | ||
| ); |
There was a problem hiding this comment.
🎯 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.
| let errorTrackingState: ErrorTrackingState | null = null; | ||
| let latestWidgetContext: WidgetErrorContext = {}; |
There was a problem hiding this comment.
🔒 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.
| const redactedString = (value: string) => | ||
| value | ||
| .replace(/(authorization)(["'`\s:=]+)([^"',\s}]+)/gi, `$1$2${redacted}`) | ||
| .replace(/(https?:\/\/)([^@\s]+)@/gi, `$1${redacted}@`); |
There was a problem hiding this comment.
🔒 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.
| 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.
| 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"], | ||
| }); |
There was a problem hiding this comment.
🔒 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/**' || trueRepository: 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.
| 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, | ||
| }); |
There was a problem hiding this comment.
🔒 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.
Resolves ENG-3743 and ENG-3744
Summary by CodeRabbit
New Features
Bug Fixes