Skip to content

Commit cacddd7

Browse files
feat: Add githubCodePreview component (#15558)
## DESCRIBE YOUR PR I wanted to link reference implementations on develop directly to github blobs and have them loaded inline (like slack/github). preview link: https://sentry-docs-git-neel-github-unfurl.sentry.dev/contributing/pages/components/#githubcodepreview ## IS YOUR CHANGE URGENT? Help us prioritize incoming PRs by letting us know when the change needs to go live. - [ ] Urgent deadline (GA date, etc.): <!-- ENTER DATE HERE --> - [ ] Other deadline: <!-- ENTER DATE HERE --> - [x] None: Not urgent, can wait up to 1 week+ ## SLA - Teamwork makes the dream work, so please add a reviewer to your PRs. - Please give the docs team up to 1 week to review your PR unless you've added an urgent due date to it. Thanks in advance for your help! ## PRE-MERGE CHECKLIST *Make sure you've checked the following before merging your changes:* - [x] Checked Vercel preview for correctness, including links - [ ] PR was reviewed and approved by any necessary SMEs (subject matter experts) - [ ] PR was reviewed and approved by a member of the [Sentry docs team](https://github.com/orgs/getsentry/teams/docs) --------- Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 39f44c5 commit cacddd7

File tree

7 files changed

+247
-9
lines changed

7 files changed

+247
-9
lines changed

docs/contributing/pages/components.mdx

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,9 @@ If `categorySupported` is specified, it will automatically hide the option when
266266
</Alert>
267267

268268
```markdown {tabTitle:Example}
269-
<SdkOption
270-
name="sampleRate"
271-
type="number"
269+
<SdkOption
270+
name="sampleRate"
271+
type="number"
272272
defaultValue="1.0"
273273
envVar="SENTRY_SAMPLE_RATE">
274274

@@ -530,3 +530,39 @@ Example:
530530
```
531531

532532
</OnboardingOption>
533+
534+
## GitHubCodePreview
535+
536+
Embed and display code directly from a GitHub repository. This component fetches code from GitHub and displays it with syntax highlighting.
537+
538+
```jsx {tabTitle:Example}
539+
<GitHubCodePreview url="https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/integrations/argv.py" />
540+
```
541+
542+
Output of above:
543+
544+
<GitHubCodePreview url="https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/integrations/argv.py" />
545+
546+
You can also link to specific lines:
547+
548+
```jsx {tabTitle:With Line Numbers}
549+
<GitHubCodePreview url="https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/client.py#L1123-L1144" />
550+
```
551+
552+
Output of above:
553+
554+
<GitHubCodePreview url="https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/client.py#L1123-L1144" />
555+
556+
Attributes:
557+
558+
- `url` (string) - **required** - GitHub blob URL with optional line numbers
559+
- Full file: `https://github.com/owner/repo/blob/main/src/file.ts`
560+
- Single line: `https://github.com/owner/repo/blob/main/src/file.ts#L10`
561+
- Line range: `https://github.com/owner/repo/blob/main/src/file.ts#L10-L20`
562+
563+
**Features:**
564+
565+
- Automatically detects language from file extension
566+
- Displays filename with line numbers (if specified)
567+
- Includes a link to view the source on GitHub
568+
- Shows loading and error states

src/components/codeBlock/code-blocks.module.scss

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@
112112
.code-actions {
113113
display: grid;
114114
height: 32px;
115-
grid-template-columns: max-content max-content;
116-
grid-gap: 1rem;
115+
grid-auto-flow: column;
116+
grid-auto-columns: max-content;
117117
align-items: center;
118118
position: absolute;
119119
margin: 0 0.25rem;
@@ -123,6 +123,7 @@
123123

124124
.filename {
125125
font-size: 0.75rem;
126+
margin-right: 1rem;
126127
color: var(--white);
127128
font-weight: normal;
128129
}

src/components/codeBlock/index.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import {RefObject, useContext, useEffect, useLayoutEffect, useRef, useState} from 'react';
4-
import {Clipboard} from 'react-feather';
4+
import {Clipboard, ExternalLink} from 'react-feather';
55

66
import {usePlausibleEvent} from 'sentry-docs/hooks/usePlausibleEvent';
77

@@ -14,6 +14,7 @@ import {updateElementsVisibilityForOptions} from '../onboarding';
1414

1515
export interface CodeBlockProps {
1616
children: React.ReactNode;
17+
externalLink?: string;
1718
filename?: string;
1819
language?: string;
1920
title?: string;
@@ -52,7 +53,7 @@ function getCopiableText(element: HTMLDivElement) {
5253
return text.trim();
5354
}
5455

55-
export function CodeBlock({filename, language, children}: CodeBlockProps) {
56+
export function CodeBlock({filename, language, children, externalLink}: CodeBlockProps) {
5657
const [showCopied, setShowCopied] = useState(false);
5758
const codeRef = useRef<HTMLDivElement>(null);
5859
const codeContext = useContext(CodeContext);
@@ -140,6 +141,17 @@ export function CodeBlock({filename, language, children}: CodeBlockProps) {
140141
<Clipboard size={16} />
141142
</button>
142143
)}
144+
{externalLink && (
145+
<a
146+
href={externalLink}
147+
target="_blank"
148+
rel="noopener noreferrer"
149+
className={styles.copy}
150+
title="View Github Source"
151+
>
152+
<ExternalLink size={16} />
153+
</a>
154+
)}
143155
</div>
144156
<div
145157
data-mdast="ignore"
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
'use client';
2+
3+
import {useEffect, useState} from 'react';
4+
5+
import {CodeBlock} from './codeBlock';
6+
import {CodeTabs} from './codeTabs';
7+
import {codeToJsx} from './highlightCode';
8+
9+
interface GitHubCodePreviewProps {
10+
/**
11+
* GitHub blob URL with optional line numbers
12+
* Examples:
13+
* - https://github.com/owner/repo/blob/main/src/file.ts#L10-L20
14+
* - https://github.com/owner/repo/blob/abc123/src/file.ts#L5
15+
*/
16+
url: string;
17+
}
18+
19+
interface ParsedGitHubUrl {
20+
owner: string;
21+
path: string;
22+
ref: string;
23+
repo: string;
24+
endLine?: number;
25+
startLine?: number;
26+
}
27+
28+
function parseGitHubUrl(url: string): ParsedGitHubUrl | null {
29+
try {
30+
const urlObj = new URL(url);
31+
32+
// Check if it's a GitHub URL
33+
if (urlObj.hostname !== 'github.com') {
34+
return null;
35+
}
36+
37+
// Parse pathname: /owner/repo/blob/ref/path/to/file
38+
const pathParts = urlObj.pathname.split('/').filter(Boolean);
39+
40+
if (pathParts.length < 5 || pathParts[2] !== 'blob') {
41+
return null;
42+
}
43+
44+
const owner = pathParts[0];
45+
const repo = pathParts[1];
46+
const ref = pathParts[3];
47+
const path = pathParts.slice(4).join('/');
48+
49+
// Parse line numbers from hash (#L10-L20 or #L10)
50+
let startLine: number | undefined;
51+
let endLine: number | undefined;
52+
53+
if (urlObj.hash) {
54+
const lineMatch = urlObj.hash.match(/^#L(\d+)(?:-L(\d+))?$/);
55+
if (lineMatch) {
56+
startLine = parseInt(lineMatch[1], 10);
57+
endLine = lineMatch[2] ? parseInt(lineMatch[2], 10) : startLine;
58+
}
59+
}
60+
61+
return {owner, repo, ref, path, startLine, endLine};
62+
} catch {
63+
return null;
64+
}
65+
}
66+
67+
function getLanguageFromPath(path: string): string {
68+
const ext = path.split('.').pop()?.toLowerCase();
69+
70+
// Map file extensions to supported languages
71+
const langMap: Record<string, string> = {
72+
js: 'javascript',
73+
jsx: 'javascript',
74+
ts: 'typescript',
75+
tsx: 'typescript',
76+
json: 'json',
77+
sh: 'bash',
78+
bash: 'bash',
79+
py: 'python',
80+
};
81+
82+
return langMap[ext || ''] || 'text';
83+
}
84+
85+
async function fetchGitHubContent(parsed: ParsedGitHubUrl): Promise<string | null> {
86+
try {
87+
// Use GitHub raw content URL - encode path segments to handle special characters
88+
const encodedPath = parsed.path
89+
.split('/')
90+
.map(segment => encodeURIComponent(segment))
91+
.join('/');
92+
const rawUrl = `https://raw.githubusercontent.com/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/${encodeURIComponent(parsed.ref)}/${encodedPath}`;
93+
94+
const response = await fetch(rawUrl);
95+
96+
if (!response.ok) {
97+
return null;
98+
}
99+
100+
const content = await response.text();
101+
102+
// Extract specific lines if specified
103+
if (parsed.startLine && parsed.endLine) {
104+
const lines = content.split('\n');
105+
const selectedLines = lines.slice(parsed.startLine - 1, parsed.endLine);
106+
return selectedLines.join('\n');
107+
}
108+
109+
return content;
110+
} catch {
111+
return null;
112+
}
113+
}
114+
115+
export function GitHubCodePreview({url}: GitHubCodePreviewProps) {
116+
const [code, setCode] = useState<string | null>(null);
117+
const [loading, setLoading] = useState(true);
118+
const [error, setError] = useState<string | null>(null);
119+
const [parsed, setParsed] = useState<ParsedGitHubUrl | null>(null);
120+
121+
useEffect(() => {
122+
const loadCode = async () => {
123+
setLoading(true);
124+
setError(null);
125+
126+
const parsedUrl = parseGitHubUrl(url);
127+
128+
if (!parsedUrl) {
129+
setError('Invalid GitHub URL');
130+
setLoading(false);
131+
return;
132+
}
133+
134+
setParsed(parsedUrl);
135+
136+
const content = await fetchGitHubContent(parsedUrl);
137+
138+
if (content === null) {
139+
setError('Failed to fetch code from GitHub');
140+
setLoading(false);
141+
return;
142+
}
143+
144+
setCode(content);
145+
setLoading(false);
146+
};
147+
148+
loadCode();
149+
}, [url]);
150+
151+
if (loading) {
152+
return (
153+
<div style={{padding: '1rem', color: 'var(--gray-500)'}}>
154+
Loading code from GitHub...
155+
</div>
156+
);
157+
}
158+
159+
if (error || !parsed || !code) {
160+
return (
161+
<div style={{padding: '1rem', color: 'var(--red-500)'}}>
162+
{error || 'Failed to load code'}
163+
</div>
164+
);
165+
}
166+
167+
const language = getLanguageFromPath(parsed.path);
168+
const filename = parsed.path.split('/').pop() || parsed.path;
169+
const lineInfo =
170+
parsed.startLine && parsed.endLine
171+
? `#L${parsed.startLine}${parsed.startLine !== parsed.endLine ? `-L${parsed.endLine}` : ''}`
172+
: '';
173+
174+
return (
175+
<CodeTabs>
176+
<CodeBlock filename={filename + lineInfo} language={language} externalLink={url}>
177+
<pre className={`language-${language}`}>
178+
<code>{codeToJsx(code, language)}</code>
179+
</pre>
180+
</CodeBlock>
181+
</CodeTabs>
182+
);
183+
}

src/components/highlightCode.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@ import {jsx, jsxs} from 'react/jsx-runtime';
33
import {toJsxRuntime} from 'hast-util-to-jsx-runtime';
44
import {Nodes} from 'hastscript/lib/create-h';
55
import bash from 'refractor/lang/bash.js';
6+
import javascript from 'refractor/lang/javascript.js';
67
import json from 'refractor/lang/json.js';
8+
import python from 'refractor/lang/python.js';
79
import typescript from 'refractor/lang/typescript.js';
810
import {refractor} from 'refractor/lib/core.js';
911

1012
refractor.register(bash);
1113
refractor.register(json);
14+
refractor.register(javascript);
1215
refractor.register(typescript);
16+
refractor.register(python);
1317

1418
// If a new language should be supported, add it here and register it in refractor above
15-
export const SUPPORTED_LANGUAGES = ['bash', 'json', 'typescript'];
19+
export const SUPPORTED_LANGUAGES = ['bash', 'json', 'javascript', 'typescript', 'python'];
1620

1721
export function codeToJsx(code: string, lang = 'json') {
1822
if (!SUPPORTED_LANGUAGES.includes(lang)) {

src/mdxComponents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {DefinitionList} from './components/definitionList';
1414
import {DevDocsCardGrid} from './components/devDocsCardGrid';
1515
import DocImage from './components/docImage';
1616
import {Expandable} from './components/expandable';
17+
import {GitHubCodePreview} from './components/githubCodePreview';
1718
import {GitHubDomainChecker} from './components/githubDomainChecker';
1819
import {GuideGrid} from './components/guideGrid';
1920
import {JsBundleList} from './components/jsBundleList';
@@ -77,6 +78,7 @@ export function mdxComponents(
7778
TableOfContents,
7879
CreateGitHubAppForm,
7980
GitHubDomainChecker,
81+
GitHubCodePreview,
8082
ConfigValue,
8183
DefinitionList,
8284
Expandable,

vercel.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
},
1818
{
1919
"key": "Content-Security-Policy",
20-
"value": "upgrade-insecure-requests; default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' *.sentry-cdn.com www.googletagmanager.com plausible.io widget.kapa.ai www.gstatic.com www.google.com vercel.live; connect-src 'self' *.sentry.io sentry.io *.algolia.net *.algolianet.com *.algolia.io kapa-widget-proxy-la7dkmplpq-uc.a.run.app plausible.io reload.getsentry.net storage.googleapis.com; img-src * 'self' data: www.google.com www.googletagmanager.com; style-src 'self' 'unsafe-inline'; font-src 'self' fonts.gstatic.com; frame-src www.google.com recaptcha.google.com demo.arcade.software player.vimeo.com; worker-src blob:; report-uri https://o1.ingest.sentry.io/api/1297620/security/?sentry_key=b3cfba5788cb4c138f855c8120f70eab; report-to csp"
20+
"value": "upgrade-insecure-requests; default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' *.sentry-cdn.com www.googletagmanager.com plausible.io widget.kapa.ai www.gstatic.com www.google.com vercel.live; connect-src 'self' *.sentry.io sentry.io *.algolia.net *.algolianet.com *.algolia.io kapa-widget-proxy-la7dkmplpq-uc.a.run.app plausible.io reload.getsentry.net storage.googleapis.com raw.githubusercontent.com; img-src * 'self' data: www.google.com www.googletagmanager.com; style-src 'self' 'unsafe-inline'; font-src 'self' fonts.gstatic.com; frame-src www.google.com recaptcha.google.com demo.arcade.software player.vimeo.com; worker-src blob:; report-uri https://o1.ingest.sentry.io/api/1297620/security/?sentry_key=b3cfba5788cb4c138f855c8120f70eab; report-to csp"
2121
},
2222
{
2323
"key": "NEL",

0 commit comments

Comments
 (0)