Repository navigation
Conversation
Add `sentryWebViewProps({ allowedHosts })` which injects the React Native
trace context (`sentry-trace`/`baggage` meta tags, via
`injectedJavaScriptBeforeContentLoaded`) into a `react-native-webview`, so the
page's `@sentry/browser` continues the same trace on pageload. Host-gated by
`allowedHosts` (case-insensitive, subdomain match) so the context never reaches
non-allow-listed pages. Pure JS, no native/bridge changes, both architectures.
Also adds a sample WebView demo screen and an iOS/Android Maestro E2E.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
🤖 This preview updates automatically when you update the PR. |
|
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📲 Install BuildsAndroid
|
iOS (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5125c43+dirty | 3846.45 ms | 1221.12 ms | -2625.32 ms |
| 5fe1c6c+dirty | 1220.79 ms | 1217.63 ms | -3.16 ms |
| 5257d80+dirty | 3854.39 ms | 1234.28 ms | -2620.11 ms |
| 0c50e94+dirty | 3863.88 ms | 1235.45 ms | -2628.43 ms |
| 3e3142c+dirty | 3867.38 ms | 1232.48 ms | -2634.90 ms |
| ae37560+dirty | 3832.10 ms | 1219.09 ms | -2613.02 ms |
| f170ec3+dirty | 3822.26 ms | 1218.33 ms | -2603.93 ms |
| 1e5d96d+dirty | 3851.45 ms | 1212.05 ms | -2639.41 ms |
| 6177334+dirty | 3834.85 ms | 1217.58 ms | -2617.28 ms |
| 5a010b7+dirty | 3838.85 ms | 1214.98 ms | -2623.87 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5125c43+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 5fe1c6c+dirty | 3.38 MiB | 4.77 MiB | 1.39 MiB |
| 5257d80+dirty | 5.15 MiB | 6.69 MiB | 1.54 MiB |
| 0c50e94+dirty | 5.15 MiB | 6.95 MiB | 1.80 MiB |
| 3e3142c+dirty | 4.98 MiB | 6.56 MiB | 1.58 MiB |
| ae37560+dirty | 5.15 MiB | 6.70 MiB | 1.54 MiB |
| f170ec3+dirty | 5.15 MiB | 6.69 MiB | 1.53 MiB |
| 1e5d96d+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| 6177334+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 5a010b7+dirty | 5.15 MiB | 6.69 MiB | 1.54 MiB |
Previous results on branch: feat/webview-trace-connection
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 38369a5+dirty | 3859.26 ms | 1234.82 ms | -2624.44 ms |
| cae40ac+dirty | 3867.38 ms | 1221.07 ms | -2646.31 ms |
| 38369a5+dirty | 3865.45 ms | 1234.82 ms | -2630.63 ms |
| 3f38932+dirty | 3848.33 ms | 1207.79 ms | -2640.54 ms |
| 42ab8cd+dirty | 3841.44 ms | 1217.98 ms | -2623.46 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 38369a5+dirty | 5.15 MiB | 6.93 MiB | 1.78 MiB |
| cae40ac+dirty | 5.15 MiB | 6.94 MiB | 1.79 MiB |
| 38369a5+dirty | 5.15 MiB | 6.93 MiB | 1.78 MiB |
| 3f38932+dirty | 5.15 MiB | 6.93 MiB | 1.78 MiB |
| 42ab8cd+dirty | 5.15 MiB | 6.95 MiB | 1.80 MiB |
Android (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 23598c3+dirty | 414.12 ms | 426.24 ms | 12.12 ms |
| bc8f61e+dirty | 442.98 ms | 506.56 ms | 63.58 ms |
| 0a9e622+dirty | 420.73 ms | 464.83 ms | 44.10 ms |
| bf168a4+dirty | 418.21 ms | 489.74 ms | 71.53 ms |
| b0d3373+dirty | 557.66 ms | 579.42 ms | 21.76 ms |
| be120d6+dirty | 536.61 ms | 552.02 ms | 15.41 ms |
| 57e0069+dirty | 413.82 ms | 453.47 ms | 39.65 ms |
| b9bebee+dirty | 438.86 ms | 452.21 ms | 13.35 ms |
| 0d9949d+dirty | 403.57 ms | 437.00 ms | 33.43 ms |
| 580fb5c+dirty | 436.34 ms | 471.63 ms | 35.28 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 23598c3+dirty | 43.75 MiB | 48.16 MiB | 4.41 MiB |
| bc8f61e+dirty | 49.74 MiB | 55.09 MiB | 5.35 MiB |
| 0a9e622+dirty | 49.74 MiB | 55.09 MiB | 5.34 MiB |
| bf168a4+dirty | 49.74 MiB | 55.09 MiB | 5.35 MiB |
| b0d3373+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| be120d6+dirty | 49.74 MiB | 55.36 MiB | 5.62 MiB |
| 57e0069+dirty | 49.74 MiB | 54.85 MiB | 5.11 MiB |
| b9bebee+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| 0d9949d+dirty | 43.75 MiB | 48.13 MiB | 4.37 MiB |
| 580fb5c+dirty | 49.74 MiB | 54.79 MiB | 5.05 MiB |
Previous results on branch: feat/webview-trace-connection
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 38369a5+dirty | 424.09 ms | 468.34 ms | 44.25 ms |
| cae40ac+dirty | 453.83 ms | 511.20 ms | 57.37 ms |
| 3f38932+dirty | 478.31 ms | 537.14 ms | 58.83 ms |
| 38369a5+dirty | 460.42 ms | 490.15 ms | 29.73 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 38369a5+dirty | 50.56 MiB | 56.51 MiB | 5.95 MiB |
| cae40ac+dirty | 50.56 MiB | 56.59 MiB | 6.03 MiB |
| 3f38932+dirty | 50.56 MiB | 56.51 MiB | 5.95 MiB |
| 38369a5+dirty | 50.56 MiB | 56.51 MiB | 5.95 MiB |
…roid `react-native-webview` only executes `injectedJavaScriptBeforeContentLoaded` on Android when an `onMessage` handler is set. `sentryWebViewProps` now returns a no-op `onMessage` so the trace-context injection also runs on Android for the documented usage; users who need their own handler spread the props first and set `onMessage` after. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Poll for the injected meta tag in the sample page: on Android the injected script runs shortly after the page's initial script, so an immediate read misses it (real apps' @sentry/browser read on pageload init, later). - Spread sentryWebViewProps before the sample's own onMessage so the sample's handler wins now that the helper returns one. - Add Android status-bar top padding to the Playground screen so the first button is not under the status bar. - Increase the button wait to 30s for slower Android cold starts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| }, | ||
| // RN's SafeAreaView is a no-op on Android, so clear the status bar explicitly | ||
| // to keep the first button tappable (and out from under the status bar). | ||
| scrollContent: { |
There was a problem hiding this comment.
Note: this change is unrelated to the core pr. It just fixes a preexisting issue were the first button of the playground tab was hidden below the status bar/camera.
iOS (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 7436d0f+dirty | 3861.51 ms | 1231.07 ms | -2630.44 ms |
| c2e182c+dirty | 3841.76 ms | 1220.76 ms | -2621.01 ms |
| 0c50e94+dirty | 3856.18 ms | 1238.30 ms | -2617.87 ms |
| 61cc206+dirty | 3822.60 ms | 1206.17 ms | -2616.43 ms |
| c151573+dirty | 3835.64 ms | 1216.10 ms | -2619.53 ms |
| 2d5d963+dirty | 3858.87 ms | 1225.92 ms | -2632.95 ms |
| 84cba31+dirty | 3830.40 ms | 1204.44 ms | -2625.96 ms |
| 4e0b819+dirty | 3828.96 ms | 1205.64 ms | -2623.32 ms |
| 68ae91b+dirty | 3836.36 ms | 1222.04 ms | -2614.32 ms |
| ed70c7f+dirty | 3869.51 ms | 1240.25 ms | -2629.26 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 7436d0f+dirty | 5.15 MiB | 6.70 MiB | 1.54 MiB |
| c2e182c+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
| 0c50e94+dirty | 5.15 MiB | 6.95 MiB | 1.80 MiB |
| 61cc206+dirty | 4.98 MiB | 6.55 MiB | 1.57 MiB |
| c151573+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 2d5d963+dirty | 5.15 MiB | 6.88 MiB | 1.73 MiB |
| 84cba31+dirty | 4.98 MiB | 6.55 MiB | 1.57 MiB |
| 4e0b819+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| 68ae91b+dirty | 4.98 MiB | 6.46 MiB | 1.48 MiB |
| ed70c7f+dirty | 5.15 MiB | 6.95 MiB | 1.80 MiB |
Previous results on branch: feat/webview-trace-connection
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 38369a5+dirty | 3836.43 ms | 1220.55 ms | -2615.87 ms |
| cae40ac+dirty | 3870.87 ms | 1227.49 ms | -2643.38 ms |
| 38369a5+dirty | 3859.80 ms | 1231.95 ms | -2627.84 ms |
| 3f38932+dirty | 3850.69 ms | 1221.91 ms | -2628.78 ms |
| 42ab8cd+dirty | 3868.29 ms | 1253.61 ms | -2614.67 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 38369a5+dirty | 5.15 MiB | 6.93 MiB | 1.78 MiB |
| cae40ac+dirty | 5.15 MiB | 6.94 MiB | 1.79 MiB |
| 38369a5+dirty | 5.15 MiB | 6.93 MiB | 1.78 MiB |
| 3f38932+dirty | 5.15 MiB | 6.93 MiB | 1.78 MiB |
| 42ab8cd+dirty | 5.15 MiB | 6.95 MiB | 1.80 MiB |
Android (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 038a6d7+dirty | 499.02 ms | 527.68 ms | 28.66 ms |
| 27d9693+dirty | 438.63 ms | 514.08 ms | 75.46 ms |
| 3d377b5+dirty | 425.38 ms | 440.67 ms | 15.30 ms |
| 0668149+dirty | 410.52 ms | 454.12 ms | 43.60 ms |
| 580fb5c+dirty | 515.72 ms | 585.38 ms | 69.66 ms |
| 0bd8916+dirty | 400.15 ms | 442.72 ms | 42.57 ms |
| 44c8b3f+dirty | 492.13 ms | 563.47 ms | 71.34 ms |
| 822d35b+dirty | 472.69 ms | 561.24 ms | 88.56 ms |
| 7d6fd3a+dirty | 435.06 ms | 458.78 ms | 23.72 ms |
| 7ff4d0f+dirty | 403.38 ms | 427.06 ms | 23.68 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 038a6d7+dirty | 48.30 MiB | 53.60 MiB | 5.30 MiB |
| 27d9693+dirty | 49.74 MiB | 55.09 MiB | 5.34 MiB |
| 3d377b5+dirty | 43.94 MiB | 49.00 MiB | 5.06 MiB |
| 0668149+dirty | 50.56 MiB | 56.59 MiB | 6.03 MiB |
| 580fb5c+dirty | 49.74 MiB | 54.79 MiB | 5.05 MiB |
| 0bd8916+dirty | 48.30 MiB | 53.57 MiB | 5.26 MiB |
| 44c8b3f+dirty | 48.30 MiB | 53.46 MiB | 5.15 MiB |
| 822d35b+dirty | 49.74 MiB | 54.84 MiB | 5.10 MiB |
| 7d6fd3a+dirty | 43.94 MiB | 49.00 MiB | 5.06 MiB |
| 7ff4d0f+dirty | 48.30 MiB | 53.60 MiB | 5.30 MiB |
Previous results on branch: feat/webview-trace-connection
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 38369a5+dirty | 478.42 ms | 535.94 ms | 57.52 ms |
| cae40ac+dirty | 468.24 ms | 523.41 ms | 55.17 ms |
| 3f38932+dirty | 506.24 ms | 560.89 ms | 54.65 ms |
| 38369a5+dirty | 453.41 ms | 480.22 ms | 26.81 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 38369a5+dirty | 50.56 MiB | 56.51 MiB | 5.95 MiB |
| cae40ac+dirty | 50.56 MiB | 56.59 MiB | 6.03 MiB |
| 3f38932+dirty | 50.56 MiB | 56.51 MiB | 5.95 MiB |
| 38369a5+dirty | 50.56 MiB | 56.51 MiB | 5.95 MiB |
Skip injection if a `sentry-trace` meta tag already exists in the page, so the trace context is never duplicated when more than one path could inject it (e.g. a future native injector alongside the RN helper, or the script running twice). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
||
| // @public | ||
| export function sentryWebViewProps(options: SentryWebViewOptions): { | ||
| injectedJavaScriptBeforeContentLoaded: string; |
There was a problem hiding this comment.
m: Thinking forward this could potentially grow into multiple events being supported. Maybe it makes sense to prepare and use a structured approach, e.g.:
| injectedJavaScriptBeforeContentLoaded: string; | |
| injectedCustomScripts: { | |
| beforeContentLoaded: string | |
| } |
There was a problem hiding this comment.
Thank you @philprime 🙇 I surfaced the underlying component api for this one but I'll consider future proofing it.
| * ``` | ||
| */ | ||
| export function sentryWebViewProps(options: SentryWebViewOptions): { | ||
| injectedJavaScriptBeforeContentLoaded: string; |
There was a problem hiding this comment.
m: See my comment above w.r.t. future-proofing
| const sentryTraceLiteral = JSON.stringify(sentryTrace); | ||
| const baggageLiteral = JSON.stringify(traceData.baggage ?? null); | ||
|
|
||
| return `(function(){try{ |
There was a problem hiding this comment.
m: Is this safe for code injection attacks? Just thinking if any of the values are from external systems/cookies etc. and could be abused by an attacker.
cc @NinjaLikesCheez as you are a security expert
There was a problem hiding this comment.
We should absolutely engage the security team as early as possible in the design of this feature - here be dragons.
There was a problem hiding this comment.
Good point 👍 Marking as blocked to get more feedback on the security side of the changes.
There was a problem hiding this comment.
Check RN-796 for a link to the security review
# Conflicts: # CHANGELOG.md
The root span name is propagated to the WebView via the `baggage` meta tag as `sentry-transaction`. It is free-form text that can carry user data (for example a parameterized URL), so remove it before the trace context leaves the app. Every other Dynamic Sampling Context entry (release, environment, sample rate, sample rand, sampled) and any third-party baggage entries are preserved, so dynamic sampling stays consistent across the connected traces. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Feed hostile values (quote/backtick/template-literal, </script>, backslash, newline/CR, U+2028/U+2029 line separators, null byte, statement + comment) and 200 random dangerous-charset strings through every value embedded in the injected script — the sentry-trace value, a baggage entry value, and an allowedHosts entry. Assert a window canary never executes and the values are only ever read back as inert meta content, proving JSON.stringify is the escaping boundary. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
51dc667 to
87043de
Compare
`sentryWebViewProps` returns a single `injectedJavaScriptBeforeContentLoaded` string. Spreading the props and then setting your own value after the spread silently replaces Sentry's injection and disables trace linking (and setting it before drops yours). Unlike `onMessage` — where any handler keeps the Android injection running — this prop cannot be safely overridden, so document that it must be concatenated with the returned string, with an example. Addresses Warden review finding 9C6-FDS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The demo compared the WebView's injected-trace readback against a live `getTraceData()` re-read on every render. Once the navigation span ends the active `sentry-trace` keeps the same trace_id but gets a new span_id, so the comparison could flip to "not connected" even when the page received the injected trace correctly — making the status and its Maestro assertion a race against the navigation span's idle timeout. Capture the trace (`useState` initializer) and the injection props (`useMemo`) once at mount, so the injected value and the comparison baseline are the same mount-time snapshot. Addresses the Warden and Bugbot review findings on WebviewScreen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
||
| if (!sentryTrace) { | ||
| debug.log(`[${WEBVIEW_LABEL}] No active trace found; skipping WebView trace injection.`); | ||
| return ''; |
There was a problem hiding this comment.
getTraceData() returns {} only when the SDK is disabled or there is no client so I think the message here (the one added with debug.log) is misleading.
| var allowed=${allowedHostsLiteral}; | ||
| var host=(window.location&&window.location.hostname||'').toLowerCase(); | ||
| if(!host)return; | ||
| if(!allowed.some(function(h){return host===h||host.slice(-(h.length+1))==='.'+h;}))return; |
There was a problem hiding this comment.
Q: allowedHosts never get normalized, right? Because I see the comparison is pretty strict (host===h) and I guess the normalization of allowedHosts needs to be done for this to work properly.
There was a problem hiding this comment.
Good idea to enhance this. Updated with 746da1a
| // gated on its own host rather than the one the WebView was opened with. The | ||
| // script is idempotent — if a `sentry-trace` meta already exists (e.g. injected | ||
| // by another path), it does nothing, so the trace context is never duplicated. | ||
| const allowedHostsLiteral = JSON.stringify((allowedHosts ?? []).map(host => host.toLowerCase())); |
There was a problem hiding this comment.
allowedHosts ?? [] seems to be a fallback for a required field. Could the field be made optional instead?
| export { graphqlIntegration } from './graphql'; | ||
| export { supabaseIntegration } from './supabase'; | ||
| export { deeplinkIntegration } from './deeplink'; | ||
| export { sentryWebViewProps } from './webview'; |
There was a problem hiding this comment.
ok, I'm a bit confused — sentryWebViewProps is not really an integration, right? Why is it then exported from integrations/exports.ts and lives under integrations?
| let payload = ''; | ||
| for (let c = 0; c < length; c++) { | ||
| payload += CHARS[Math.floor(Math.random() * CHARS.length)]; | ||
| } |
There was a problem hiding this comment.
a small thing: Math.random() makes it non-deterministic. Could it be used with a seed instead?
- Move the webview helper out of `integrations/` (it is not an integration) to `src/js/tracing/webview.ts` and re-export it via the tracing barrel. The public API is unchanged. - Normalize `allowedHosts` entries (trim, strip scheme/path/port/trailing dot, lowercase) so bare-hostname matching is robust. - Replace the `allowedHosts ?? []` fallback on the required field with an explicit validation guard that degrades to a no-op with a warning instead of throwing during render. - Reword the no-trace debug log to reflect the real cause (the SDK being disabled or not initialized, not "no active trace"). - Make the fuzz test deterministic with a seeded PRNG and add tests for the normalization and empty-allowedHosts cases. Addresses review feedback from @alwx. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CodeQL flagged `/[/?#].*$/` in `normalizeHost` as a polynomial regular expression (backtracking on inputs with many `#`). Replace it with a `search` + `slice` that strips everything from the first path/query/fragment separator without a `.*` to backtrack on. Normalization output is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b2eb4fa. Configure here.
The JSDoc claimed the trace tags are written "before @sentry/browser initializes", which only holds on iOS. On Android `react-native-webview` injects asynchronously as the page starts, so the tags can land after the page's first synchronous <head> scripts. Reword to describe the real timing: a @sentry/browser loaded the usual way (script tag, bundle, or async/deferred init) reads the tags once present and continues the trace; only a synchronous inline init at the very top of <head> may miss them on Android and start a new trace (which fails safe — the WebView just gets its own trace). Verified on a Pixel 8 emulator: real @sentry/browser continued the trace 5/5 runs; a probe confirmed the meta is not present at the first synchronous head script. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nnection # Conflicts: # CHANGELOG.md
`normalizeHost` stripped a trailing `:<digits>` unconditionally, mistaking an unbracketed IPv6 hextet for a port (`::1` -> `:`, `2001:db8::1` -> `2001:db8:`), so an IPv6-hosted page could never match and the trace was skipped. Browsers bracket IPv6 in `location.hostname` (e.g. `[::1]`), so: keep a bracketed host up to the closing bracket (dropping any `:port` after it), wrap an unbracketed IPv6 literal (more than one colon) in brackets so it still matches, and keep normal `:port` stripping for hostnames and IPv4. Still no `.*` regex (CodeQL-safe). Addresses Warden review finding on allowedHosts normalization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

📢 Type of change
📜 Description
Adds the ability to inject the React Native SDK's current trace context into a webview.
Usage:
The
sentry-tracemetatag is used to propagate the distributed tracing.Alternatives considered
Also considered the wrapper component approach but decided to proceed with an implementation using props.
react-native-webview<WebView>, custom wrappers, refs'use dom')domprop:dom={{ ...sentryWebViewProps(...) }}domtakes a props object, not a componentinjectedJavaScriptBeforeContentLoaded(low-sensitivity, spreadable)💡 Motivation and Context
Closes #6779
Closes #6780
Closes #6781
💚 How did you test it?
📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps