From 5017d87357f5d0dcc28bba89190fdc5aad0dfd5b Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 7 Aug 2025 17:15:28 -0400 Subject: [PATCH 1/4] feat(onboarding): Add logs to react router onboarding --- .../onboarding/productSelection.tsx | 1 + .../javascript/react-router.spec.tsx | 108 +++++++++++ .../javascript/react-router.tsx | 175 +++++++++++------- 3 files changed, 213 insertions(+), 71 deletions(-) create mode 100644 static/app/gettingStartedDocs/javascript/react-router.spec.tsx diff --git a/static/app/components/onboarding/productSelection.tsx b/static/app/components/onboarding/productSelection.tsx index 837ba0cf9f86d7..7cf0c1488c51f5 100644 --- a/static/app/components/onboarding/productSelection.tsx +++ b/static/app/components/onboarding/productSelection.tsx @@ -137,6 +137,7 @@ export const platformProductAvailability = { 'javascript-react-router': [ ProductSolution.PERFORMANCE_MONITORING, ProductSolution.SESSION_REPLAY, + ProductSolution.LOGS, ], 'javascript-vue': [ ProductSolution.PERFORMANCE_MONITORING, diff --git a/static/app/gettingStartedDocs/javascript/react-router.spec.tsx b/static/app/gettingStartedDocs/javascript/react-router.spec.tsx new file mode 100644 index 00000000000000..fd5864fdc1fc6e --- /dev/null +++ b/static/app/gettingStartedDocs/javascript/react-router.spec.tsx @@ -0,0 +1,108 @@ +import {renderWithOnboardingLayout} from 'sentry-test/onboarding/renderWithOnboardingLayout'; +import {screen} from 'sentry-test/reactTestingLibrary'; +import {textWithMarkupMatcher} from 'sentry-test/utils'; + +import {ProductSolution} from 'sentry/components/onboarding/gettingStartedDoc/types'; + +import docs from './react-router'; + +describe('javascript-react-router onboarding docs', function () { + it('renders onboarding docs correctly', () => { + renderWithOnboardingLayout(docs); + + // Renders main headings + expect(screen.getByRole('heading', {name: 'Install'})).toBeInTheDocument(); + expect(screen.getByRole('heading', {name: 'Configure SDK'})).toBeInTheDocument(); + expect( + screen.getByRole('heading', {name: /Upload Source Maps/i}) + ).toBeInTheDocument(); + expect(screen.getByRole('heading', {name: 'Verify'})).toBeInTheDocument(); + + // Includes Sentry import statement in multiple configure sections + expect( + screen.getAllByText( + textWithMarkupMatcher(/import \* as Sentry from ["']@sentry\/react-router["'];/) + ) + ).toHaveLength(4); + }); + + it('displays sample rates by default', () => { + renderWithOnboardingLayout(docs, { + selectedProducts: [ + ProductSolution.ERROR_MONITORING, + ProductSolution.PERFORMANCE_MONITORING, + ProductSolution.SESSION_REPLAY, + ], + }); + + expect( + screen.getAllByText(textWithMarkupMatcher(/tracesSampleRate/)).length + ).toBeGreaterThan(0); + expect( + screen.getAllByText(textWithMarkupMatcher(/replaysSessionSampleRate/)).length + ).toBeGreaterThan(0); + expect( + screen.getAllByText(textWithMarkupMatcher(/replaysOnErrorSampleRate/)).length + ).toBeGreaterThan(0); + }); + + it('enables performance by setting tracesSampleRate to 1 and adding integration', () => { + renderWithOnboardingLayout(docs, { + selectedProducts: [ + ProductSolution.ERROR_MONITORING, + ProductSolution.PERFORMANCE_MONITORING, + ], + }); + + expect( + screen.getAllByText(textWithMarkupMatcher(/tracesSampleRate: 1\.0/)).length + ).toBeGreaterThan(0); + expect( + screen.getByText(textWithMarkupMatcher(/Sentry\.reactRouterTracingIntegration\(\)/)) + ).toBeInTheDocument(); + }); + + it('enables replay by setting replay sample rates', () => { + renderWithOnboardingLayout(docs, { + selectedProducts: [ + ProductSolution.ERROR_MONITORING, + ProductSolution.SESSION_REPLAY, + ], + }); + + expect( + screen.getByText(textWithMarkupMatcher(/replaysSessionSampleRate: 0\.1/)) + ).toBeInTheDocument(); + expect( + screen.getByText(textWithMarkupMatcher(/replaysOnErrorSampleRate: 1\.0/)) + ).toBeInTheDocument(); + }); + + it('enables profiling by setting profiling sample rates', () => { + renderWithOnboardingLayout(docs, { + selectedProducts: [ProductSolution.ERROR_MONITORING, ProductSolution.PROFILING], + }); + + expect( + screen.getByText(textWithMarkupMatcher(/profilesSampleRate: 1\.0/)) + ).toBeInTheDocument(); + expect( + screen.getByText(textWithMarkupMatcher(/nodeProfilingIntegration\(\)/)) + ).toBeInTheDocument(); + }); + + it('enables logs by setting enableLogs to true and shows logger usage in verify step', () => { + renderWithOnboardingLayout(docs, { + selectedProducts: [ProductSolution.ERROR_MONITORING, ProductSolution.LOGS], + }); + + expect( + screen.getAllByText(textWithMarkupMatcher(/enableLogs: true/)).length + ).toBeGreaterThan(0); + + // When logs are selected, verify step includes a Sentry logger call + expect( + screen.getByText(textWithMarkupMatcher(/Sentry\.logger\.info\(/)) + ).toBeInTheDocument(); + }); +}); diff --git a/static/app/gettingStartedDocs/javascript/react-router.tsx b/static/app/gettingStartedDocs/javascript/react-router.tsx index 84dd56022e67c3..5b6f0bc2ddf28f 100644 --- a/static/app/gettingStartedDocs/javascript/react-router.tsx +++ b/static/app/gettingStartedDocs/javascript/react-router.tsx @@ -25,61 +25,68 @@ import {getNodeAgentMonitoringOnboarding} from 'sentry/utils/gettingStartedDocs/ type Params = DocsParams; -const getClientSetupSnippet = (params: Params) => ` -import * as Sentry from "@sentry/react-router"; -import { startTransition, StrictMode } from "react"; -import { hydrateRoot } from "react-dom/client"; -import { HydratedRouter } from "react-router/dom"; +const getClientSetupSnippet = (params: Params) => { + const logsSnippet = params.isLogsSelected + ? ` + // Enable logs to be sent to Sentry + enableLogs: true,` + : ''; -Sentry.init({ - dsn: "${params.dsn.public}", + const performanceSnippet = params.isPerformanceSelected + ? ` + tracesSampleRate: 1.0, // Capture 100% of the transactions + // Set \`tracePropagationTargets\` to declare which URL(s) should have trace propagation enabled + tracePropagationTargets: [/^\\//, /^https:\\/\\/yourserver\\.io\\/api/],` + : ''; - // Adds request headers and IP for users, for more info visit: - // https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#sendDefaultPii - sendDefaultPii: true, + const replaySnippet = params.isReplaySelected + ? ` + replaysSessionSampleRate: 0.1, // Capture 10% of all sessions + replaysOnErrorSampleRate: 1.0, // Capture 100% of sessions with an error` + : ''; - integrations: [${ - params.isPerformanceSelected - ? ` - // Performance - Sentry.reactRouterTracingIntegration(),` - : '' - }${ - params.isReplaySelected - ? ` + const integrationsList = []; + + if (params.isPerformanceSelected) { + integrationsList.push(` + // Tracing + Sentry.reactRouterTracingIntegration(),`); + } + + if (params.isReplaySelected) { + integrationsList.push(` // Session Replay - Sentry.replayIntegration(${getReplayConfigOptions(params.replayOptions)}),` - : '' - }${ - params.isFeedbackSelected - ? ` + Sentry.replayIntegration(${getReplayConfigOptions(params.replayOptions)}),`); + } + + if (params.isFeedbackSelected) { + integrationsList.push(` // User Feedback Sentry.feedbackIntegration({ // Additional SDK configuration goes in here, for example: colorScheme: "system", ${getFeedbackConfigOptions(params.feedbackOptions)} - }),` - : '' + }),`); } - ],${ - params.isPerformanceSelected - ? ` - - tracesSampleRate: 1.0, // Capture 100% of the transactions - // Set \`tracePropagationTargets\` to declare which URL(s) should have trace propagation enabled - tracePropagationTargets: [/^\\//, /^https:\\/\\/yourserver\\.io\\/api/],` - : '' - }${ - params.isReplaySelected + const integrationsCode = + integrationsList.length > 0 ? ` + integrations: [${integrationsList.join('')} + ]` + : ''; - // Capture Replay for 10% of all sessions, - // plus 100% of sessions with an error - replaysSessionSampleRate: 0.1, - replaysOnErrorSampleRate: 1.0,` - : '' - } + return ` +import * as Sentry from "@sentry/react-router"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; +import { HydratedRouter } from "react-router/dom"; + +Sentry.init({ + dsn: "${params.dsn.public}", + // Adds request headers and IP for users, for more info visit: + // https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#sendDefaultPii + sendDefaultPii: true,${integrationsCode}${logsSnippet}${performanceSnippet}${replaySnippet} }); startTransition(() => { @@ -90,6 +97,7 @@ startTransition(() => { ); });`; +}; const getRootErrorBoundarySnippet = () => ` import * as Sentry from "@sentry/react-router"; @@ -127,37 +135,53 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { ); }`; -const getServerSetupSnippet = (params: Params) => ` -import * as Sentry from "@sentry/react-router";${ - params.isProfilingSelected +const getServerSetupSnippet = (params: Params) => { + const logsSnippet = params.isLogsSelected + ? ` + // Enable logs to be sent to Sentry + enableLogs: true,` + : ''; + + const performanceSnippet = params.isPerformanceSelected + ? ` + tracesSampleRate: 1.0, // Capture 100% of the transactions` + : ''; + + const profilingSnippet = params.isProfilingSelected + ? ` + profilesSampleRate: 1.0, // profile every transaction` + : ''; + + const profilingImport = params.isProfilingSelected ? ` import { nodeProfilingIntegration } from '@sentry/profiling-node';` - : '' -} + : ''; + + const integrationsList = []; + + if (params.isProfilingSelected) { + integrationsList.push(` + // Profiling + nodeProfilingIntegration(),`); + } + + const integrationsCode = + integrationsList.length > 0 + ? ` + integrations: [${integrationsList.join('')} + ]` + : ''; + + return ` +import * as Sentry from "@sentry/react-router";${profilingImport} Sentry.init({ dsn: "${params.dsn.public}", - // Adds request headers and IP for users, for more info visit: // https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#sendDefaultPii - sendDefaultPii: true,${ - params.isProfilingSelected - ? ` - - integrations: [nodeProfilingIntegration()],` - : '' - }${ - params.isPerformanceSelected - ? ` - tracesSampleRate: 1.0, // Capture 100% of the transactions` - : '' - }${ - params.isProfilingSelected - ? ` - profilesSampleRate: 1.0, // profile every transaction` - : '' - } + sendDefaultPii: true,${integrationsCode}${logsSnippet}${performanceSnippet}${profilingSnippet} });`; +}; const getServerEntrySnippet = () => ` import * as Sentry from '@sentry/react-router'; @@ -178,21 +202,30 @@ export const handleError: HandleErrorFunction = (error, { request }) => { // React Router may abort some interrupted requests, don't log those if (!request.signal.aborted) { Sentry.captureException(error); - // optionally log the error so you can see it + // optionally log the error to the console so you can see it console.error(error); } };`; -const getVerifySnippet = () => ` +const getVerifySnippet = (params: Params) => { + const logsCode = params.isLogsSelected + ? ` + // Send a log before throwing the error + Sentry.logger.info("User triggered test error", { + 'action': SentryLogAttribute.string('test_loader_error'), + });` + : ''; + return ` import type { Route } from "./+types/error-page"; -export async function loader() { +export async function loader() {${logsCode} throw new Error("Sentry Test Error"); } export default function ErrorPage() { return
This page will throw an error!
; }`; +}; const getPackageJsonScriptsSnippet = () => ` { @@ -297,7 +330,7 @@ const onboarding: OnboardingConfig = { }, { description: tct( - 'Initialize the Sentry React Router SDK in your [code:entry.client.tsx] file:', + 'Initialize the Sentry React Router SDK in your [code:entry.client.tsx] file, above where you call [code:hydrateRoot]:', {code: } ), title: t('Client Setup'), @@ -369,7 +402,7 @@ const onboarding: OnboardingConfig = { collapsible: true, }, ], - verify: () => [ + verify: params => [ { type: StepType.VERIFY, description: t( @@ -378,7 +411,7 @@ const onboarding: OnboardingConfig = { configurations: [ { language: 'tsx', - code: getVerifySnippet(), + code: getVerifySnippet(params), }, ], }, From adda12a7a786126a093487f54845d3dec48791ac Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 8 Aug 2025 12:00:52 -0400 Subject: [PATCH 2/4] Update static/app/gettingStartedDocs/javascript/react-router.tsx --- static/app/gettingStartedDocs/javascript/react-router.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/gettingStartedDocs/javascript/react-router.tsx b/static/app/gettingStartedDocs/javascript/react-router.tsx index 5b6f0bc2ddf28f..e0f374a5d04382 100644 --- a/static/app/gettingStartedDocs/javascript/react-router.tsx +++ b/static/app/gettingStartedDocs/javascript/react-router.tsx @@ -212,7 +212,7 @@ const getVerifySnippet = (params: Params) => { ? ` // Send a log before throwing the error Sentry.logger.info("User triggered test error", { - 'action': SentryLogAttribute.string('test_loader_error'), + 'action': 'test_loader_error', });` : ''; return ` From 85dde446e89a88ac69393d6c658f04710ca2793a Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 8 Aug 2025 12:05:27 -0400 Subject: [PATCH 3/4] trailing comma --- static/app/gettingStartedDocs/javascript/react-router.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/gettingStartedDocs/javascript/react-router.tsx b/static/app/gettingStartedDocs/javascript/react-router.tsx index e0f374a5d04382..f7a868d87642e3 100644 --- a/static/app/gettingStartedDocs/javascript/react-router.tsx +++ b/static/app/gettingStartedDocs/javascript/react-router.tsx @@ -73,7 +73,7 @@ const getClientSetupSnippet = (params: Params) => { integrationsList.length > 0 ? ` integrations: [${integrationsList.join('')} - ]` + ],` : ''; return ` From b7b143dc73c1f4626936983eca12b3a03e2715d4 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 8 Aug 2025 12:06:21 -0400 Subject: [PATCH 4/4] other comma --- static/app/gettingStartedDocs/javascript/react-router.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/gettingStartedDocs/javascript/react-router.tsx b/static/app/gettingStartedDocs/javascript/react-router.tsx index f7a868d87642e3..5f94ef07ed2862 100644 --- a/static/app/gettingStartedDocs/javascript/react-router.tsx +++ b/static/app/gettingStartedDocs/javascript/react-router.tsx @@ -169,7 +169,7 @@ import { nodeProfilingIntegration } from '@sentry/profiling-node';` integrationsList.length > 0 ? ` integrations: [${integrationsList.join('')} - ]` + ],` : ''; return `