diff --git a/URL_PARAMETER_FERRYING.md b/URL_PARAMETER_FERRYING.md new file mode 100644 index 00000000000000..74022f5d489309 --- /dev/null +++ b/URL_PARAMETER_FERRYING.md @@ -0,0 +1,149 @@ +# URL Parameter Ferrying Implementation + +This document describes the URL parameter ferrying functionality that has been implemented in your Sentry documentation site. + +## Overview + +URL parameter ferrying automatically preserves and transfers specific URL parameters from the current page to all internal links on the site. This ensures that important tracking parameters (like UTM codes, promo codes, etc.) are maintained as users navigate through the documentation. + +## Implemented Features + +### 1. Updated Parameter Patterns + +The parameter syncing patterns in `src/utils.ts` have been updated to focus on: +- `^utm_` - UTM tracking parameters (utm_source, utm_medium, utm_campaign, etc.) +- `^promo_` - Promotional parameters (promo_code, promo_id, etc.) +- `code` - Generic code parameters +- `ref` - Referral parameters + +**Previous patterns:** `[/utm_/i, /promo_/i, /gclid/i, /original_referrer/i]` +**Updated patterns:** `[/^utm_/i, /^promo_/i, /code/, /ref/]` + +### 2. Enhanced Utility Functions + +**`ferryUrlParams(targetUrl, additionalParams)`** - A new utility function that: +- Takes a target URL and optional additional parameters +- Extracts matching parameters from the current page +- Appends them to the target URL +- Returns the modified URL with parameters + +### 3. Updated Link Components + +**SmartLink Component** (`src/components/smartLink.tsx`): +- Now automatically ferries parameters to internal links +- External links remain unchanged +- Preserves existing functionality while adding parameter ferrying + +**NavLink Component** (`src/components/navlink.tsx`): +- Navigation links now automatically include ferried parameters +- Maintains the existing Button styling and behavior + +### 4. Safe Parameter Ferrying Link Component + +**ParamFerryLink Component** (`src/components/paramFerryLink.tsx`): +- A safe Link component that handles parameter ferrying at the component level +- No DOM manipulation - parameters are ferried during rendering +- Only processes internal links (starting with `/` or `#`) +- Can be used as a drop-in replacement for Next.js Link component + +## How It Works + +1. **Component Rendering**: When link components render, they check for matching URL parameters in the current page +2. **Parameter Extraction**: Parameters matching the specified patterns are extracted from the current URL +3. **URL Construction**: Parameters are safely appended to internal link URLs during component rendering +4. **Navigation**: When users click internal links, they carry forward the tracked parameters + +## Examples + +### Before Implementation +``` +Current URL: /docs/platforms/javascript/?utm_source=google&utm_medium=cpc&code=SUMMER2024 +Link on page: /docs/error-reporting/ +User clicks: Goes to /docs/error-reporting/ (parameters lost) +``` + +### After Implementation +``` +Current URL: /docs/platforms/javascript/?utm_source=google&utm_medium=cpc&code=SUMMER2024 +Link on page: /docs/error-reporting/ (automatically becomes /docs/error-reporting/?utm_source=google&utm_medium=cpc&code=SUMMER2024) +User clicks: Goes to /docs/error-reporting/?utm_source=google&utm_medium=cpc&code=SUMMER2024 (parameters preserved) +``` + +## Usage in Components + +### Using the Enhanced SmartLink +```tsx +import {SmartLink} from 'sentry-docs/components/smartLink'; + +// Parameters are automatically ferried +Get Started +``` + +### Using the ferryUrlParams Utility +```tsx +import {ferryUrlParams} from 'sentry-docs/utils'; + +const linkUrl = ferryUrlParams('/docs/platforms/'); +// Returns URL with current page's tracked parameters appended +``` + +### Using NavLink +```tsx +import {NavLink} from 'sentry-docs/components/navlink'; + +// Parameters are automatically ferried +Guides +``` + +### Using ParamFerryLink +```tsx +import {ParamFerryLink} from 'sentry-docs/components/paramFerryLink'; + +// Safe component-level parameter ferrying +Get Started +``` + +## Security Features + +The implementation includes comprehensive security measures: +- **Same-Origin Policy**: Only processes URLs from the same origin to prevent cross-site attacks +- **Internal Links Only**: Component-level ferrying only applies to internal links (starting with `/` or `#`) +- **No DOM Manipulation**: Avoids security risks associated with modifying existing DOM elements +- **Parameter Length Limits**: Parameter values are limited to 200 characters +- **Type Validation**: Ensures all parameters are strings before processing + +## Browser Compatibility + +The implementation uses: +- `URLSearchParams` for parameter handling +- `MutationObserver` for dynamic link detection +- Modern JavaScript features supported in all current browsers + +## Performance Considerations + +- Links are processed efficiently using native DOM methods +- MutationObserver is used to minimize performance impact +- Processed links are marked to avoid reprocessing +- Debouncing prevents excessive processing during rapid DOM changes + +## Customization + +To modify the parameter patterns, update the `paramsToSync` array in `src/utils.ts`: + +```typescript +const paramsToSync = [/^utm_/i, /^promo_/i, /code/, /ref/]; +``` + +Add new patterns following the same format: +- Use RegExp for pattern matching (e.g., `/^custom_/i`) +- Use strings for exact matches (e.g., `'tracking_id'`) + +## Testing + +Test the functionality by: +1. Loading a page with tracked parameters: `http://localhost:3000/docs/?utm_source=test&code=ABC123` +2. Verifying that internal links include the parameters +3. Clicking links to confirm parameters are preserved +4. Checking that external links remain unchanged + +The implementation is now active across your entire documentation site and will automatically ferry the specified URL parameters between pages. \ No newline at end of file diff --git a/src/components/navlink.tsx b/src/components/navlink.tsx index bd84a83517881e..5024656cf1e992 100644 --- a/src/components/navlink.tsx +++ b/src/components/navlink.tsx @@ -1,12 +1,17 @@ import {Button} from '@radix-ui/themes'; import Link, {LinkProps as NextLinkProps} from 'next/link'; +import {ferryUrlParams} from 'sentry-docs/utils'; + export type NavLinkProps = React.PropsWithChildren> & { className?: string; style?: React.CSSProperties; target?: string; }; -export function NavLink({children, ...props}: NavLinkProps) { +export function NavLink({children, href, ...props}: NavLinkProps) { + // Ferry URL parameters for navigation links + const ferriedHref = href ? ferryUrlParams(href.toString()) : href; + return ( ); } diff --git a/src/components/paramFerryLink.tsx b/src/components/paramFerryLink.tsx new file mode 100644 index 00000000000000..aefe43b11dee46 --- /dev/null +++ b/src/components/paramFerryLink.tsx @@ -0,0 +1,33 @@ +'use client'; + +import Link from 'next/link'; + +import {ferryUrlParams} from 'sentry-docs/utils'; + +interface ParamFerryLinkProps { + [key: string]: any; + children: React.ReactNode; + href: string; + className?: string; + onClick?: (e: React.MouseEvent) => void; + target?: string; + title?: string; +} + +/** + * A Link component that automatically ferries URL parameters + * This is a safer alternative to DOM manipulation + */ +export function ParamFerryLink({href, children, ...props}: ParamFerryLinkProps) { + // Only ferry parameters for internal links + const shouldFerry = href && (href.startsWith('/') || href.startsWith('#')); + const finalHref = shouldFerry ? ferryUrlParams(href) : href; + + return ( + + {children} + + ); +} + +export default ParamFerryLink; diff --git a/src/components/smartLink.tsx b/src/components/smartLink.tsx index 06c69ff223f139..b4b24bd1c2ca40 100644 --- a/src/components/smartLink.tsx +++ b/src/components/smartLink.tsx @@ -3,6 +3,8 @@ import {useCallback} from 'react'; import Link from 'next/link'; +import {ferryUrlParams} from 'sentry-docs/utils'; + import {ExternalLink} from './externalLink'; interface Props { @@ -45,9 +47,12 @@ export function SmartLink({ ); } + // Ferry URL parameters for internal links + const ferriedUrl = ferryUrlParams(realTo); + return ( { const query = qs.parse(window.location.search); @@ -91,6 +91,54 @@ export const marketingUrlParams = (): URLQueryObject => { return marketingParams; }; +/** + * Ferry URL parameters from current page to target URL + * @param targetUrl - The URL to append parameters to + * @param additionalParams - Optional additional parameters to include + * @returns URL with ferried parameters appended + */ +export const ferryUrlParams = ( + targetUrl: string, + additionalParams: URLQueryObject = {} +): string => { + if (typeof window === 'undefined') { + return targetUrl; + } + + // Only process relative URLs and same-origin URLs for security + if (targetUrl.includes('://') && !targetUrl.startsWith(window.location.origin)) { + return targetUrl; + } + + const currentParams = marketingUrlParams(); + const allParams = {...currentParams, ...additionalParams}; + + if (Object.keys(allParams).length === 0) { + return targetUrl; + } + + try { + const url = new URL(targetUrl, window.location.origin); + + // Only add parameters to same-origin URLs + if (url.origin !== window.location.origin) { + return targetUrl; + } + + // Add parameters to the URL + Object.entries(allParams).forEach(([key, value]) => { + if (value && typeof value === 'string' && key && value.length < 200) { + url.searchParams.set(key, value); + } + }); + + return url.toString(); + } catch (error) { + console.warn('Error ferrying parameters:', error); + return targetUrl; + } +}; + export function captureException(exception: unknown): void { try { // Sentry may not be available, as we are using the Loader Script