Skip to content

Commit 22b8b41

Browse files
committed
Add DSN comments and project-specific clipboard messages
1 parent cdb16bc commit 22b8b41

File tree

5 files changed

+277
-4
lines changed

5 files changed

+277
-4
lines changed

IMPLEMENTATION_SUMMARY.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Implementation Summary: DSN Comments and Clipboard Improvements
2+
3+
This document summarizes the implementation of the GitHub issue [#13015](https://github.com/getsentry/sentry-docs/issues/13015) which requested improvements to the way users interact with DSN snippets in code examples.
4+
5+
## Changes Implemented
6+
7+
### 1. Enhanced Clipboard Functionality with Project Names
8+
9+
**Modified Files:**
10+
- `src/components/codeBlock/index.tsx`
11+
- `src/components/apiExamples/apiExamples.tsx`
12+
13+
**Changes:**
14+
- Added `CodeContext` integration to access current project information
15+
- Modified clipboard "Copied" message to show "Copied for [project name]" instead of just "Copied"
16+
- Falls back to "Copied" if no project context is available
17+
18+
**Code Changes:**
19+
```typescript
20+
// Get the current project name for the copied message
21+
const getCurrentProjectName = () => {
22+
if (!codeContext) {
23+
return null;
24+
}
25+
26+
const {codeKeywords, sharedKeywordSelection} = codeContext;
27+
const [sharedSelection] = sharedKeywordSelection;
28+
const currentSelectionIdx = sharedSelection['PROJECT'] ?? 0;
29+
const currentProject = codeKeywords?.PROJECT?.[currentSelectionIdx];
30+
31+
return currentProject?.title;
32+
};
33+
34+
const projectName = getCurrentProjectName();
35+
const copiedMessage = projectName ? `Copied for ${projectName}` : 'Copied';
36+
```
37+
38+
### 2. Automatic DSN Comments in Code Examples
39+
40+
**New File Created:**
41+
- `src/remark-dsn-comments.js`
42+
43+
**Modified Files:**
44+
- `src/mdx.ts` (added plugin to processing pipeline)
45+
46+
**Functionality:**
47+
- Automatically adds helpful comments above DSN patterns in code blocks
48+
- Supports multiple programming languages with appropriate comment syntax:
49+
- JavaScript/TypeScript: `// Hover over the DSN to see your project, or click it to select a different one`
50+
- Python/Ruby/Shell: `# Hover over the DSN to see your project, or click it to select a different one`
51+
- Java/C/C++/etc.: `// Hover over the DSN to see your project, or click it to select a different one`
52+
- HTML/XML: `<!-- Hover over the DSN to see your project, or click it to select a different one -->`
53+
- CSS: `/* Hover over the DSN to see your project, or click it to select a different one */`
54+
- YAML/TOML: `# Hover over the DSN to see your project, or click it to select a different one`
55+
56+
**Processing Logic:**
57+
- Uses AST (Abstract Syntax Tree) processing via remark plugin
58+
- Detects `___PROJECT.DSN___` patterns in code blocks
59+
- Adds language-appropriate comments above DSN lines
60+
- Prevents duplicate comments if they already exist
61+
- Skips JSON files (which don't support comments)
62+
63+
## How It Works
64+
65+
### Project Context Integration
66+
The existing `CodeContext` system already provides:
67+
- Current selected project information via `sharedKeywordSelection`
68+
- Project titles formatted as "org-name / project-name"
69+
- Hover tooltips on DSN values showing project names
70+
71+
### Remark Plugin Processing
72+
The new `remarkDsnComments` plugin is integrated into the MDX processing pipeline:
73+
1. Processes all code blocks during build time
74+
2. Searches for DSN patterns using regex: `/___PROJECT\.DSN___/g`
75+
3. Determines appropriate comment syntax based on language
76+
4. Inserts comments above DSN lines
77+
5. Prevents duplicate comments
78+
79+
### Language Support
80+
The plugin supports all major programming languages used in Sentry documentation:
81+
- C-style languages (JavaScript, TypeScript, Java, C++, etc.)
82+
- Python-style languages (Python, Ruby, Shell, YAML, etc.)
83+
- Web languages (HTML, CSS, XML)
84+
- Configuration formats (TOML, YAML)
85+
86+
## User Experience Improvements
87+
88+
### Before
89+
- Users saw generic "Copied" message when copying code
90+
- No guidance about DSN hover functionality
91+
- Users had to discover project selection feature on their own
92+
93+
### After
94+
- Users see "Copied for [specific-project]" message, confirming which project the code is for
95+
- Clear instructions appear above DSN in code examples
96+
- Better discoverability of hover/click functionality for project selection
97+
98+
## Testing
99+
100+
The implementation has been added to the codebase but couldn't be fully tested due to missing production environment variables. However, the code changes are:
101+
102+
1. **Type-safe**: Using existing TypeScript interfaces and patterns
103+
2. **Backward-compatible**: Falls back gracefully when project context is unavailable
104+
3. **Performance-conscious**: Uses existing context without additional API calls
105+
4. **Consistent**: Follows existing code patterns and styling
106+
107+
## Files Modified
108+
109+
### Core Implementation
110+
- `src/remark-dsn-comments.js` (new)
111+
- `src/components/codeBlock/index.tsx`
112+
- `src/components/apiExamples/apiExamples.tsx`
113+
- `src/mdx.ts`
114+
115+
### No Breaking Changes
116+
- All changes are additive and backward-compatible
117+
- Existing functionality remains unchanged
118+
- New features enhance user experience without disrupting current workflows
119+
120+
## Future Considerations
121+
122+
1. **Testing**: Unit tests could be added for the remark plugin
123+
2. **Customization**: Comment text could be made configurable if needed
124+
3. **Internationalization**: Comment text could be localized for different languages
125+
4. **Analytics**: Could track usage of the enhanced clipboard functionality
126+
127+
The implementation successfully addresses both requirements from the GitHub issue:
128+
1. ✅ Added helpful comments above DSN in code examples
129+
2. ✅ Enhanced clipboard messages to include specific project names

src/components/apiExamples/apiExamples.tsx

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

3-
import {Fragment, useEffect, useState} from 'react';
3+
import {Fragment, useContext, useEffect, useState} from 'react';
44
import {Clipboard} from 'react-feather';
55

66
import {type API} from 'sentry-docs/build/resolveOpenAPI';
@@ -9,6 +9,7 @@ import codeBlockStyles from '../codeBlock/code-blocks.module.scss';
99
import styles from './apiExamples.module.scss';
1010

1111
import {CodeBlock} from '../codeBlock';
12+
import {CodeContext} from '../codeContext';
1213
import {CodeTabs} from '../codeTabs';
1314
import {codeToJsx} from '../highlightCode';
1415

@@ -62,12 +63,32 @@ export function ApiExamples({api}: Props) {
6263
useEffect(() => {
6364
setShowCopyButton(true);
6465
}, []);
66+
67+
const codeContext = useContext(CodeContext);
68+
6569
async function copyCode(code: string) {
6670
await navigator.clipboard.writeText(code);
6771
setShowCopied(true);
6872
setTimeout(() => setShowCopied(false), 1200);
6973
}
7074

75+
// Get the current project name for the copied message
76+
const getCurrentProjectName = () => {
77+
if (!codeContext) {
78+
return null;
79+
}
80+
81+
const {codeKeywords, sharedKeywordSelection} = codeContext;
82+
const [sharedSelection] = sharedKeywordSelection;
83+
const currentSelectionIdx = sharedSelection['PROJECT'] ?? 0;
84+
const currentProject = codeKeywords?.PROJECT?.[currentSelectionIdx];
85+
86+
return currentProject?.title;
87+
};
88+
89+
const projectName = getCurrentProjectName();
90+
const copiedMessage = projectName ? `Copied for ${projectName}` : 'Copied';
91+
7192
let exampleJson: any;
7293
if (api.responses[selectedResponse].content?.examples) {
7394
exampleJson = Object.values(
@@ -134,7 +155,7 @@ export function ApiExamples({api}: Props) {
134155
className={codeBlockStyles.copied}
135156
style={{opacity: showCopied ? 1 : 0}}
136157
>
137-
Copied
158+
{copiedMessage}
138159
</div>
139160
{selectedTabView === 0 &&
140161
(exampleJson ? (

src/components/codeBlock/index.tsx

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

3-
import {RefObject, useEffect, useRef, useState} from 'react';
3+
import {RefObject, useContext, useEffect, useRef, useState} from 'react';
44
import {Clipboard} from 'react-feather';
55

66
import styles from './code-blocks.module.scss';
77

8+
import {CodeContext} from '../codeContext';
89
import {makeHighlightBlocks} from '../codeHighlights';
910
import {makeKeywordsClickable} from '../codeKeywords';
1011

@@ -26,6 +27,8 @@ export function CodeBlock({filename, language, children}: CodeBlockProps) {
2627
setShowCopyButton(true);
2728
}, []);
2829

30+
const codeContext = useContext(CodeContext);
31+
2932
useCleanSnippetInClipboard(codeRef, {language});
3033

3134
async function copyCodeOnClick() {
@@ -45,6 +48,23 @@ export function CodeBlock({filename, language, children}: CodeBlockProps) {
4548
}
4649
}
4750

51+
// Get the current project name for the copied message
52+
const getCurrentProjectName = () => {
53+
if (!codeContext) {
54+
return null;
55+
}
56+
57+
const {codeKeywords, sharedKeywordSelection} = codeContext;
58+
const [sharedSelection] = sharedKeywordSelection;
59+
const currentSelectionIdx = sharedSelection['PROJECT'] ?? 0;
60+
const currentProject = codeKeywords?.PROJECT?.[currentSelectionIdx];
61+
62+
return currentProject?.title;
63+
};
64+
65+
const projectName = getCurrentProjectName();
66+
const copiedMessage = projectName ? `Copied for ${projectName}` : 'Copied';
67+
4868
return (
4969
<div className={styles['code-block']}>
5070
<div className={styles['code-actions']}>
@@ -60,7 +80,7 @@ export function CodeBlock({filename, language, children}: CodeBlockProps) {
6080
className={styles.copied}
6181
style={{opacity: showCopied ? 1 : 0}}
6282
>
63-
Copied
83+
{copiedMessage}
6484
</div>
6585
<div ref={codeRef}>
6686
{makeKeywordsClickable(makeHighlightBlocks(children, language))}

src/mdx.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import remarkVariables from './remark-variables';
4343
import {FrontMatter, Platform, PlatformConfig} from './types';
4444
import {isNotNil} from './utils';
4545
import {isVersioned, VERSION_INDICATOR} from './versioning';
46+
import remarkDsnComments from './remark-dsn-comments';
4647

4748
type SlugFile = {
4849
frontMatter: Platform & {slug: string};
@@ -563,6 +564,7 @@ export async function getFileBySlug(slug: string): Promise<SlugFile> {
563564
[remarkTocHeadings, {exportRef: toc}],
564565
remarkGfm,
565566
remarkDefList,
567+
remarkDsnComments,
566568
remarkFormatCodeBlocks,
567569
[remarkImageSize, {sourceFolder: cwd, publicFolder: path.join(root, 'public')}],
568570
remarkMdxImages,

src/remark-dsn-comments.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import {visit} from 'unist-util-visit';
2+
3+
const DSN_PATTERN = /___PROJECT\.DSN___/g;
4+
5+
export default function remarkDsnComments() {
6+
return tree => {
7+
visit(tree, 'code', node => {
8+
if (!node.value || !DSN_PATTERN.test(node.value)) {
9+
return;
10+
}
11+
12+
// Reset the regex for the next match
13+
DSN_PATTERN.lastIndex = 0;
14+
15+
// Add comment above DSN based on language
16+
const language = node.lang || '';
17+
let comment = '';
18+
19+
switch (language) {
20+
case 'javascript':
21+
case 'typescript':
22+
case 'jsx':
23+
case 'tsx':
24+
comment = '// Hover over the DSN to see your project, or click it to select a different one';
25+
break;
26+
case 'python':
27+
case 'ruby':
28+
case 'shell':
29+
case 'bash':
30+
comment = '# Hover over the DSN to see your project, or click it to select a different one';
31+
break;
32+
case 'java':
33+
case 'kotlin':
34+
case 'swift':
35+
case 'dart':
36+
case 'csharp':
37+
case 'c':
38+
case 'cpp':
39+
comment = '// Hover over the DSN to see your project, or click it to select a different one';
40+
break;
41+
case 'php':
42+
comment = '// Hover over the DSN to see your project, or click it to select a different one';
43+
break;
44+
case 'go':
45+
comment = '// Hover over the DSN to see your project, or click it to select a different one';
46+
break;
47+
case 'rust':
48+
comment = '// Hover over the DSN to see your project, or click it to select a different one';
49+
break;
50+
case 'yaml':
51+
case 'yml':
52+
comment = '# Hover over the DSN to see your project, or click it to select a different one';
53+
break;
54+
case 'toml':
55+
comment = '# Hover over the DSN to see your project, or click it to select a different one';
56+
break;
57+
case 'html':
58+
case 'xml':
59+
comment = '<!-- Hover over the DSN to see your project, or click it to select a different one -->';
60+
break;
61+
case 'css':
62+
comment = '/* Hover over the DSN to see your project, or click it to select a different one */';
63+
break;
64+
case 'json':
65+
// JSON doesn't support comments, so we skip it
66+
return;
67+
default:
68+
// For unknown languages, try to use a common comment style
69+
comment = '// Hover over the DSN to see your project, or click it to select a different one';
70+
break;
71+
}
72+
73+
// Find the line with DSN and add comment above it
74+
const lines = node.value.split('\n');
75+
let modified = false;
76+
77+
for (let i = 0; i < lines.length; i++) {
78+
if (DSN_PATTERN.test(lines[i])) {
79+
// Reset regex for next potential match
80+
DSN_PATTERN.lastIndex = 0;
81+
82+
// Check that we don't already have a comment above this line
83+
const commentAlreadyExists = i > 0 &&
84+
(lines[i - 1].includes('Hover over the DSN') ||
85+
lines[i - 1].includes('hover over the dsn'));
86+
87+
if (!commentAlreadyExists) {
88+
// Insert comment before the DSN line
89+
lines.splice(i, 0, comment);
90+
modified = true;
91+
i++; // Skip the newly inserted comment line
92+
}
93+
}
94+
}
95+
96+
if (modified) {
97+
node.value = lines.join('\n');
98+
}
99+
});
100+
};
101+
}

0 commit comments

Comments
 (0)