Skip to content

Commit 39a0302

Browse files
antonisclaude
andcommitted
feat(tracing): Track WebView trace-propagation feature adoption
`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 <noreply@anthropic.com>
1 parent 60c29d1 commit 39a0302

3 files changed

Lines changed: 83 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
99
## Unreleased
1010

11+
### Internal
12+
13+
- Track `sentryWebViewProps` adoption via a feature marker ([#PRNUM](https://github.com/getsentry/sentry-react-native/pull/PRNUM))
14+
1115
### Dependencies
1216

1317
- Bump CLI from v3.8.1 to v3.8.2 ([#6933](https://github.com/getsentry/sentry-react-native/pull/6933))

‎packages/core/src/js/tracing/webview.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@ import type { SerializedTraceData } from '@sentry/core';
22

33
import { debug, getTraceData, objectToBaggageHeader, parseBaggageHeader } from '@sentry/core';
44

5+
import { registerFeatureMarker } from '../utils/featureMarkers';
6+
57
const WEBVIEW_LABEL = 'Sentry WebView';
68

9+
/**
10+
* Name registered as a no-op integration when the WebView helper is used, so the
11+
* feature flows through to `event.sdk.integrations` — the channel Sentry uses to
12+
* measure feature adoption across SDKs. Kept in sync with the equivalent marker on
13+
* the other SDKs (Apple, Android, Flutter).
14+
*/
15+
export const WEBVIEW_INTEGRATION_NAME = 'WebViewTracePropagation';
16+
717
/**
818
* `baggage` key carrying the root span's name. It is free-form text and can
919
* contain user data (for example a parameterized URL), so it is removed from the
@@ -85,6 +95,12 @@ export function sentryWebViewProps(options: SentryWebViewOptions): {
8595
injectedJavaScriptBeforeContentLoaded: string;
8696
onMessage: () => void;
8797
} {
98+
// Record feature adoption. `sentryWebViewProps` is a helper rather than an
99+
// integration, so without this it would be invisible to adoption telemetry. The
100+
// marker is registered on use (not on injection success) so it reflects that the
101+
// app wired up the feature. Idempotent and fails quietly — never breaks rendering.
102+
registerFeatureMarker(WEBVIEW_INTEGRATION_NAME);
103+
88104
return {
89105
injectedJavaScriptBeforeContentLoaded: createSentryWebViewInjection(options),
90106
// Required on Android: `react-native-webview` only runs injected JavaScript

‎packages/core/test/tracing/webview.test.ts‎

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
import { getTraceData } from '@sentry/core';
1+
import { getClient, getTraceData } from '@sentry/core';
22

3-
import { sentryWebViewProps } from '../../src/js/tracing/webview';
3+
import { sentryWebViewProps, WEBVIEW_INTEGRATION_NAME } from '../../src/js/tracing/webview';
44

55
jest.mock('@sentry/core', () => {
66
const actual = jest.requireActual('@sentry/core');
77
return {
88
...actual,
99
getTraceData: jest.fn(),
10+
getClient: jest.fn(),
1011
};
1112
});
1213

1314
const mockGetTraceData = getTraceData as jest.Mock;
15+
const mockGetClient = getClient as jest.Mock;
1416

1517
const SENTRY_TRACE = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1';
1618
const BAGGAGE = 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-environment=prod';
@@ -407,3 +409,62 @@ describe('sentryWebViewProps', () => {
407409
});
408410
});
409411
});
412+
413+
describe('sentryWebViewProps feature adoption marker', () => {
414+
afterEach(() => {
415+
mockGetClient.mockReset();
416+
mockGetTraceData.mockReset();
417+
});
418+
419+
it('registers the WebView feature marker on the client when used', () => {
420+
const integrations: Record<string, { name: string }> = {};
421+
mockGetClient.mockReturnValue({
422+
getIntegrationByName: (name: string) => integrations[name],
423+
addIntegration: (integration: { name: string }) => {
424+
integrations[integration.name] = integration;
425+
},
426+
});
427+
mockGetTraceData.mockReturnValue({ 'sentry-trace': SENTRY_TRACE, baggage: BAGGAGE });
428+
429+
sentryWebViewProps({ allowedHosts: ['example.com'] });
430+
431+
// The marker flows into `event.sdk.integrations`, the feature-adoption channel.
432+
expect(integrations[WEBVIEW_INTEGRATION_NAME]).toEqual({ name: WEBVIEW_INTEGRATION_NAME });
433+
});
434+
435+
it('registers the marker on use even when there is no active trace to inject', () => {
436+
const addIntegration = jest.fn();
437+
mockGetClient.mockReturnValue({ getIntegrationByName: () => undefined, addIntegration });
438+
// No trace -> the injection is a no-op, but adoption is still recorded.
439+
mockGetTraceData.mockReturnValue({});
440+
441+
sentryWebViewProps({ allowedHosts: ['example.com'] });
442+
443+
expect(addIntegration).toHaveBeenCalledWith({ name: WEBVIEW_INTEGRATION_NAME });
444+
});
445+
446+
it('does not register twice (idempotent)', () => {
447+
const addIntegration = jest.fn();
448+
let registered = false;
449+
mockGetClient.mockReturnValue({
450+
getIntegrationByName: () => (registered ? { name: WEBVIEW_INTEGRATION_NAME } : undefined),
451+
addIntegration: (integration: { name: string }) => {
452+
registered = true;
453+
addIntegration(integration);
454+
},
455+
});
456+
mockGetTraceData.mockReturnValue({ 'sentry-trace': SENTRY_TRACE });
457+
458+
sentryWebViewProps({ allowedHosts: ['example.com'] });
459+
sentryWebViewProps({ allowedHosts: ['example.com'] });
460+
461+
expect(addIntegration).toHaveBeenCalledTimes(1);
462+
});
463+
464+
it('does not throw when no client is available', () => {
465+
mockGetClient.mockReturnValue(undefined);
466+
mockGetTraceData.mockReturnValue({ 'sentry-trace': SENTRY_TRACE });
467+
468+
expect(() => sentryWebViewProps({ allowedHosts: ['example.com'] })).not.toThrow();
469+
});
470+
});

0 commit comments

Comments
 (0)