-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Ferry url params between pages #14138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
elijames-codecov
wants to merge
4
commits into
master
from
cursor/ferry-url-params-between-pages-61fb
Closed
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
0501138
Implement URL parameter ferrying across documentation site links
cursoragent 3305ecc
Enhance URL parameter ferrying with robust security measures
cursoragent 54f2a37
Replace ParamFerry component with safer ParamFerryLink component
cursoragent 7bb839b
[getsentry/action-github-commit] Auto commit
getsantry[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| # 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. Automatic Parameter Ferrying Component | ||
|
|
||
| **ParamFerry Component** (`src/components/paramFerry.tsx`): | ||
| - A client-side component that automatically processes all links on the page | ||
| - Uses MutationObserver to handle dynamically added links | ||
| - Skips external links, anchors, mailto, and tel links | ||
| - Marks processed links to avoid duplicate processing | ||
|
|
||
| **Added to Layout** (`app/layout.tsx`): | ||
| - The ParamFerry component is included in the root layout | ||
| - Works automatically across all pages without additional setup | ||
|
|
||
| ## How It Works | ||
|
|
||
| 1. **Page Load**: When a page loads with URL parameters matching the patterns, the ParamFerry component identifies them | ||
| 2. **Link Processing**: All internal links on the page are processed to include the relevant parameters | ||
| 3. **Dynamic Updates**: New links added to the page (via JavaScript) are automatically processed | ||
| 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 | ||
| <SmartLink href="/docs/getting-started/">Get Started</SmartLink> | ||
| ``` | ||
|
|
||
| ### 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 | ||
| <NavLink href="/docs/guides/">Guides</NavLink> | ||
| ``` | ||
|
|
||
| ## Security Features | ||
|
|
||
| The implementation includes comprehensive security measures: | ||
| - **URL Scheme Validation**: Blocks dangerous URL schemes (`javascript:`, `data:`, `vbscript:`, `file:`, `about:`) | ||
| - **Parameter Sanitization**: Sanitizes parameter keys and values to prevent injection attacks | ||
| - **Length Limits**: Parameter values are limited to 500 characters | ||
| - **Control Character Filtering**: Removes control characters from parameters | ||
| - **Multiple Validation Layers**: URLs are validated at multiple stages of 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| 'use client'; | ||
|
|
||
| import {useEffect} from 'react'; | ||
|
|
||
| /** | ||
| * ParamFerry component that automatically adds tracked URL parameters to all links on the page | ||
| * Focuses on utm_, promo_, code, and ref parameters | ||
| */ | ||
| export default function ParamFerry(): null { | ||
| useEffect(() => { | ||
| if (typeof window === 'undefined') { | ||
| return; | ||
| } | ||
|
|
||
| // Define parameter patterns to ferry | ||
| const paramsToSync = [/^utm_/i, /^promo_/i, /code/, /ref/]; | ||
|
|
||
| // Get current URL parameters | ||
| const getCurrentParams = () => { | ||
| const urlParams = new URLSearchParams(window.location.search); | ||
| const params: Record<string, string> = {}; | ||
|
|
||
| urlParams.forEach((value, key) => { | ||
| const shouldSync = paramsToSync.some(pattern => { | ||
| if (pattern instanceof RegExp) { | ||
| return pattern.test(key); | ||
| } | ||
| return key === pattern; | ||
| }); | ||
|
|
||
| if (shouldSync && typeof value === 'string' && typeof key === 'string') { | ||
| // Sanitize parameter key and value during collection | ||
| const sanitizedKey = key.replace(/[^\w\-._~]/g, ''); | ||
| const sanitizedValue = value.replace(/[\r\n\t]/g, '').substring(0, 500); | ||
|
|
||
| if (sanitizedKey && sanitizedValue) { | ||
| params[sanitizedKey] = sanitizedValue; | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| return params; | ||
| }; | ||
|
|
||
| // Validate URL is safe to process | ||
| const isSafeUrl = (url: string): boolean => { | ||
| // Block dangerous schemes | ||
| const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:', 'about:']; | ||
| const lowerUrl = url.toLowerCase().trim(); | ||
|
|
||
| if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) { | ||
| return false; | ||
| } | ||
|
|
||
| // Only allow http, https, and relative URLs | ||
| if (lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://') || lowerUrl.startsWith('/') || !lowerUrl.includes(':')) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| }; | ||
|
|
||
| // Ferry parameters to a URL | ||
| const ferryParams = (targetUrl: string) => { | ||
| // Validate URL safety first | ||
| if (!isSafeUrl(targetUrl)) { | ||
| return targetUrl; | ||
| } | ||
|
|
||
| const params = getCurrentParams(); | ||
|
|
||
| if (Object.keys(params).length === 0) { | ||
| return targetUrl; | ||
| } | ||
|
|
||
| try { | ||
| const url = new URL(targetUrl, window.location.origin); | ||
|
|
||
| // Double-check the constructed URL is safe | ||
| if (!isSafeUrl(url.toString())) { | ||
| return targetUrl; | ||
| } | ||
|
|
||
| // Add parameters to the URL with validation | ||
| Object.entries(params).forEach(([key, value]) => { | ||
| if (value && typeof value === 'string') { | ||
| // Sanitize parameter key and value | ||
| const sanitizedKey = key.replace(/[^\w\-._~]/g, ''); | ||
| const sanitizedValue = value.replace(/[\r\n\t]/g, '').substring(0, 500); // Limit length and remove control characters | ||
|
|
||
| if (sanitizedKey && sanitizedValue) { | ||
| url.searchParams.set(sanitizedKey, sanitizedValue); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| const result = url.toString(); | ||
|
|
||
| // Final safety check | ||
| if (!isSafeUrl(result)) { | ||
| return targetUrl; | ||
| } | ||
|
|
||
| return result; | ||
| } catch (error) { | ||
| console.warn('Error ferrying parameters:', error); | ||
| return targetUrl; | ||
| } | ||
| }; | ||
|
|
||
| const processLinks = () => { | ||
| // Get all anchor tags on the page | ||
| const links = document.querySelectorAll('a[href]'); | ||
|
|
||
| links.forEach(link => { | ||
| const anchor = link as HTMLAnchorElement; | ||
| const href = anchor.getAttribute('href'); | ||
| if (!href) return; | ||
|
|
||
| // Skip if already processed | ||
| if (anchor.hasAttribute('data-param-ferried')) return; | ||
|
|
||
| // Skip external links, anchors, mailto, tel links, and unsafe URLs | ||
| if ( | ||
| (href.startsWith('http') && !href.includes(window.location.hostname)) || | ||
| href.startsWith('#') || | ||
| href.startsWith('mailto:') || | ||
| href.startsWith('tel:') || | ||
| !isSafeUrl(href) | ||
| ) { | ||
| anchor.setAttribute('data-param-ferried', 'skip'); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const ferriedUrl = ferryParams(href); | ||
|
|
||
| if (ferriedUrl !== href && isSafeUrl(ferriedUrl)) { | ||
| // For relative URLs, extract just the path and search params | ||
| if (!href.startsWith('http')) { | ||
| const url = new URL(ferriedUrl); | ||
| const newHref = url.pathname + url.search + url.hash; | ||
| // Final validation before setting href | ||
| if (isSafeUrl(newHref)) { | ||
| anchor.setAttribute('href', newHref); | ||
| } | ||
| } else { | ||
| // Final validation before setting href | ||
| if (isSafeUrl(ferriedUrl)) { | ||
| anchor.setAttribute('href', ferriedUrl); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| anchor.setAttribute('data-param-ferried', 'true'); | ||
| } catch (error) { | ||
| console.warn('Error ferrying parameters for link:', href, error); | ||
| anchor.setAttribute('data-param-ferried', 'error'); | ||
| } | ||
| }); | ||
| }; | ||
|
|
||
| // Process links immediately | ||
| processLinks(); | ||
|
|
||
| // Set up a MutationObserver to handle dynamically added links | ||
| const observer = new MutationObserver(() => { | ||
| // Debounce to avoid excessive processing | ||
| setTimeout(processLinks, 10); | ||
| }); | ||
|
|
||
| // Start observing | ||
| observer.observe(document.body, { | ||
| childList: true, | ||
| subtree: true | ||
| }); | ||
|
|
||
| // Cleanup | ||
| return () => { | ||
| observer.disconnect(); | ||
| }; | ||
| }, []); | ||
|
|
||
| return null; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.