Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/build/open-api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,20 @@ type Tag = {
'x-sidebar-name': string;
};

type ServerMeta = {
description: string;
export type ServerVariable = {
default: string;
description?: string;
enum?: string[];
};

export type ServerMeta = {
description?: string;
url: string;
variables?: Record<string, ServerVariable>;
};

export type DeRefedOpenAPI = {
servers?: ServerMeta[];
paths: {
[key: string]: {
[key: string]: {
Expand Down
16 changes: 10 additions & 6 deletions src/build/resolveOpenAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import {promises as fs} from 'fs';

import {DeRefedOpenAPI} from './open-api/types';
import {
type DeRefedOpenAPI,
type ServerMeta,
type ServerVariable,
} from './open-api/types';

// SENTRY_API_SCHEMA_SHA is used in the sentry-docs GHA workflow in getsentry/sentry-api-schema.
// DO NOT change variable name unless you change it in the sentry-docs GHA workflow in getsentry/sentry-api-schema.
Expand Down Expand Up @@ -75,6 +79,7 @@ export type API = {
descriptionMarkdown?: string;
requestBodyContent?: any;
security?: {[key: string]: string[]};
serverVariables?: Record<string, ServerVariable>;
summary?: string;
};

Expand Down Expand Up @@ -133,17 +138,16 @@ async function apiCategoriesUncached(): Promise<APICategory[]> {
const isDeprecated = isDeprecatedOperationId(apiData.operationId);
const cleanOperationId = stripDeprecatedPrefix(apiData.operationId ?? '');

let server = 'https://sentry.io';
if (apiData.servers && apiData.servers[0]) {
server = apiData.servers[0].url;
}
const resolvedServer: ServerMeta = apiData.servers?.[0] ??
data.servers?.[0] ?? {url: 'https://sentry.io'};
apiData.tags.forEach(tag => {
categoryMap[tag].apis.push({
apiPath,
method,
name: cleanOperationId,
deprecated: isDeprecated,
server,
server: resolvedServer.url,
serverVariables: resolvedServer.variables,
slug: slugify(cleanOperationId),
summary: apiData.summary,
descriptionMarkdown: apiData.description,
Expand Down
59 changes: 59 additions & 0 deletions src/components/apiExamples/apiExamples.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,62 @@
margin: 0;
}
}

.region-selector {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
font-size: 0.8rem;
}

.region-label {
color: var(--text-color);
font-weight: 500;
}

.region-tabs {
display: flex;
border-radius: 3px;
overflow: hidden;
border: 1px solid var(--border-color);
}

.region-tab {
background: var(--gray-100);
color: var(--text-color);
border: none;
border-right: 1px solid var(--border-color);
padding: 0.2rem 0.6rem;
cursor: pointer;
font-size: 0.75rem;
font-weight: 500;
transition: background-color 0.15s, color 0.15s;

&:last-child {
border-right: none;
}

&:hover:not(.region-tab-selected) {
background: var(--gray-200);
}
}

:global(.dark) .region-tab {
background: #2d2d2d;
color: #9990ab;
}

:global(.dark) .region-tab:hover:not(.region-tab-selected) {
background: #3d3d3d;
}

.region-tab-selected {
background: var(--accent);
color: var(--white);
}

:global(.dark) .region-tab-selected {
background: var(--accent);
color: var(--white);
}
71 changes: 69 additions & 2 deletions src/components/apiExamples/apiExamples.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
'use client';

import {Fragment, useEffect, useState} from 'react';
import {Fragment, useContext, useEffect, useMemo, useState} from 'react';
import {Clipboard} from 'react-feather';
import {type API} from 'sentry-docs/build/resolveOpenAPI';

import {CodeBlock} from '../codeBlock';
import codeBlockStyles from '../codeBlock/code-blocks.module.scss';
import {CodeContext} from '../codeContext';
import {CodeTabs} from '../codeTabs';
import {codeToJsx} from '../highlightCode';
import styles from './apiExamples.module.scss';
Expand All @@ -18,13 +19,63 @@ const strFormat = (str: string) => {
return s + '.';
};

function resolveServerUrl(
serverTemplate: string,
variables: Record<string, {default: string; enum?: string[]}> | undefined,
overrides?: Record<string, string>
): string {
if (!variables) {
return serverTemplate;
}
let url = serverTemplate;
for (const [name, variable] of Object.entries(variables)) {
const value = overrides?.[name] ?? variable.default;
url = url.replace(`{${name}}`, value);
}
return url;
}

function detectRegionFromApiUrl(apiUrl: string): string | undefined {
const match = apiUrl.match(/^https?:\/\/(\w+)\.sentry\.io\//);
return match?.[1];
}

type Props = {
api: API;
};

export function ApiExamples({api}: Props) {
const codeContext = useContext(CodeContext);
const regionVar = api.serverVariables?.region;
const regionOptions = useMemo(() => regionVar?.enum ?? [], [regionVar?.enum]);

const [sharedSelection] = codeContext?.sharedKeywordSelection ?? [
{} as Record<string, number>,
];
const projectIdx = sharedSelection.PROJECT ?? 0;
const selectedProject =
codeContext?.codeKeywords.USER && codeContext.codeKeywords.PROJECT[projectIdx]
? codeContext.codeKeywords.PROJECT[projectIdx]
: undefined;
Comment thread
cursor[bot] marked this conversation as resolved.

const userRegion = selectedProject
? detectRegionFromApiUrl(selectedProject.API_URL)
: undefined;

const [selectedRegion, setSelectedRegion] = useState(regionVar?.default ?? 'us');

useEffect(() => {
if (userRegion && regionOptions.includes(userRegion)) {
setSelectedRegion(userRegion);
}
}, [userRegion, regionOptions]);

const resolvedServer = resolveServerUrl(api.server, api.serverVariables, {
region: selectedRegion,
});

const apiExample = [
`curl ${api.server}${api.apiPath}`,
`curl ${resolvedServer}${api.apiPath}`,
` -H 'Authorization: Bearer <auth_token>'`,
];
if (['put', 'options', 'delete'].includes(api.method.toLowerCase())) {
Expand Down Expand Up @@ -84,6 +135,22 @@ export function ApiExamples({api}: Props) {

return (
<Fragment>
{regionOptions.length > 1 && (
<div className={styles['region-selector']}>
<span className={styles['region-label']}>Region</span>
<div className={styles['region-tabs']}>
{regionOptions.map(region => (
<button
key={region}
className={`${styles['region-tab']} ${selectedRegion === region ? styles['region-tab-selected'] : ''}`}
onClick={() => setSelectedRegion(region)}
>
{region.toUpperCase()}
</button>
))}
</div>
</div>
)}
<CodeTabs>
<CodeBlock language="bash">
<pre>{codeToJsx(apiExample.join(' \\\n'), 'bash')}</pre>
Expand Down
Loading