From 8951080631329d6c4cbc3e8b1ed77fe7c5f0ecc0 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 23 Oct 2026 15:16:30 +0200 Subject: [PATCH] feat(tracing): Track WebView trace-propagation feature adoption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sentryWebViewProps` is a helper rather than an integration, so it was invisible to Sentry's feature-adoption telemetry. Register a no-op `WebViewTracePropagation` marker when the helper is used, so the feature flows through to `event.sdk.integrations` — the channel used to measure adoption across SDKs (same approach as GlobalErrorBoundary, NavigationContainer, MobileReplay). Idempotent and fails quietly. Follow-up to #6794. Aligns with the WebView adoption markers planned for the Apple, Android, and Flutter SDKs. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 4 ++ packages/core/src/js/tracing/webview.ts | 16 ++++++ packages/core/test/tracing/webview.test.ts | 65 +++++++++++++++++++++- 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3185190039..cfe12a27b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ## Unreleased +### Internal + +- Track `sentryWebViewProps` adoption via a feature marker ([#6938](https://github.com/getsentry/sentry-react-native/pull/6938)) + ### Dependencies - Bump CLI from v3.8.1 to v3.8.2 ([#6933](https://github.com/getsentry/sentry-react-native/pull/6933)) diff --git a/packages/core/src/js/tracing/webview.ts b/packages/core/src/js/tracing/webview.ts index 12b3fb38a4..666481e5dd 100644 --- a/packages/core/src/js/tracing/webview.ts +++ b/packages/core/src/js/tracing/webview.ts @@ -2,8 +2,18 @@ import type { SerializedTraceData } from '@sentry/core'; import { debug, getTraceData, objectToBaggageHeader, parseBaggageHeader } from '@sentry/core'; +import { registerFeatureMarker } from '../utils/featureMarkers'; + const WEBVIEW_LABEL = 'Sentry WebView'; +/** + * Name registered as a no-op integration when the WebView helper is used, so the + * feature flows through to `event.sdk.integrations` — the channel Sentry uses to + * measure feature adoption across SDKs. Kept in sync with the equivalent marker on + * the other SDKs (Apple, Android, Flutter). + */ +export const WEBVIEW_INTEGRATION_NAME = 'WebViewTracePropagation'; + /** * `baggage` key carrying the root span's name. It is free-form text and can * contain user data (for example a parameterized URL), so it is removed from the @@ -85,6 +95,12 @@ export function sentryWebViewProps(options: SentryWebViewOptions): { injectedJavaScriptBeforeContentLoaded: string; onMessage: () => void; } { + // Record feature adoption. `sentryWebViewProps` is a helper rather than an + // integration, so without this it would be invisible to adoption telemetry. The + // marker is registered on use (not on injection success) so it reflects that the + // app wired up the feature. Idempotent and fails quietly — never breaks rendering. + registerFeatureMarker(WEBVIEW_INTEGRATION_NAME); + return { injectedJavaScriptBeforeContentLoaded: createSentryWebViewInjection(options), // Required on Android: `react-native-webview` only runs injected JavaScript diff --git a/packages/core/test/tracing/webview.test.ts b/packages/core/test/tracing/webview.test.ts index fac381215c..6abed4f558 100644 --- a/packages/core/test/tracing/webview.test.ts +++ b/packages/core/test/tracing/webview.test.ts @@ -1,16 +1,18 @@ -import { getTraceData } from '@sentry/core'; +import { getClient, getTraceData } from '@sentry/core'; -import { sentryWebViewProps } from '../../src/js/tracing/webview'; +import { sentryWebViewProps, WEBVIEW_INTEGRATION_NAME } from '../../src/js/tracing/webview'; jest.mock('@sentry/core', () => { const actual = jest.requireActual('@sentry/core'); return { ...actual, getTraceData: jest.fn(), + getClient: jest.fn(), }; }); const mockGetTraceData = getTraceData as jest.Mock; +const mockGetClient = getClient as jest.Mock; const SENTRY_TRACE = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; const BAGGAGE = 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-environment=prod'; @@ -407,3 +409,62 @@ describe('sentryWebViewProps', () => { }); }); }); + +describe('sentryWebViewProps feature adoption marker', () => { + afterEach(() => { + mockGetClient.mockReset(); + mockGetTraceData.mockReset(); + }); + + it('registers the WebView feature marker on the client when used', () => { + const integrations: Record = {}; + mockGetClient.mockReturnValue({ + getIntegrationByName: (name: string) => integrations[name], + addIntegration: (integration: { name: string }) => { + integrations[integration.name] = integration; + }, + }); + mockGetTraceData.mockReturnValue({ 'sentry-trace': SENTRY_TRACE, baggage: BAGGAGE }); + + sentryWebViewProps({ allowedHosts: ['example.com'] }); + + // The marker flows into `event.sdk.integrations`, the feature-adoption channel. + expect(integrations[WEBVIEW_INTEGRATION_NAME]).toEqual({ name: WEBVIEW_INTEGRATION_NAME }); + }); + + it('registers the marker on use even when there is no active trace to inject', () => { + const addIntegration = jest.fn(); + mockGetClient.mockReturnValue({ getIntegrationByName: () => undefined, addIntegration }); + // No trace -> the injection is a no-op, but adoption is still recorded. + mockGetTraceData.mockReturnValue({}); + + sentryWebViewProps({ allowedHosts: ['example.com'] }); + + expect(addIntegration).toHaveBeenCalledWith({ name: WEBVIEW_INTEGRATION_NAME }); + }); + + it('does not register twice (idempotent)', () => { + const addIntegration = jest.fn(); + let registered = false; + mockGetClient.mockReturnValue({ + getIntegrationByName: () => (registered ? { name: WEBVIEW_INTEGRATION_NAME } : undefined), + addIntegration: (integration: { name: string }) => { + registered = true; + addIntegration(integration); + }, + }); + mockGetTraceData.mockReturnValue({ 'sentry-trace': SENTRY_TRACE }); + + sentryWebViewProps({ allowedHosts: ['example.com'] }); + sentryWebViewProps({ allowedHosts: ['example.com'] }); + + expect(addIntegration).toHaveBeenCalledTimes(1); + }); + + it('does not throw when no client is available', () => { + mockGetClient.mockReturnValue(undefined); + mockGetTraceData.mockReturnValue({ 'sentry-trace': SENTRY_TRACE }); + + expect(() => sentryWebViewProps({ allowedHosts: ['example.com'] })).not.toThrow(); + }); +});