diff --git a/.github/labels.yml b/.github/labels.yml index 32b592d0fb0a7..7b2776731cc95 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -7,151 +7,65 @@ # # Read more here: https://github.com/Financial-Times/github-label-sync#label-config-file -# Unito - notion.so/52d10dbfad474328850319e48b057a5b +# Unito - www.notion.so/52d10dbfad474328850319e48b057a5b - name: 'Sync: Jira' - aliases: [] color: A0CABD description: apply to auto-create a Jira shadow ticket # Dependabot - name: dependencies - aliases: [] color: F6F6F8 description: Pull requests that update a dependency file -# Status for use with stalebot and routing/triage -- name: 'Status: As Designed' - aliases: [] - color: 8D5494 - description: '' -- name: 'Status: Backlog' - aliases: [] - color: 8D5494 - description: stalebot will ignore -- name: 'Status: Done' - aliases: [] - color: 8D5494 - description: '' -- name: 'Status: In Progress' - aliases: [] - color: 8D5494 - description: stalebot will ignore -- name: 'Status: Invalid' - aliases: [] - color: 8D5494 - description: '' -- name: 'Status: Needs More Information' - aliases: [] - color: 8D5494 - description: stalebot will close after four weeks of inactivity -- name: 'Status: Stale' - aliases: [] - color: 8D5494 - description: stalebot will close after another week of inactivity -- name: 'Status: Unrouted' - aliases: [] - color: 8D5494 - description: stalebot will close after four weeks of inactivity -- name: 'Status: Untriaged' - aliases: [] - color: 8D5494 - description: stalebot will close after four weeks of inactivity -- name: 'Status: Won''t Fix' - aliases: [] - color: 8D5494 - description: '' - # Impacts - name: 'Impact: Large' - aliases: [] color: C83852 - description: '' - name: 'Impact: Medium' - aliases: [] color: FFB287 - description: '' - name: 'Impact: Small' - aliases: [] color: '452650' - description: '' # Platforms - name: 'Platform: .NET' - aliases: [] color: '584774' - description: '' - name: 'Platform: Android' - aliases: [] color: '584774' - description: '' - name: 'Platform: Capacitor' - aliases: [] color: '584774' - description: '' - name: 'Platform: Cocoa' - aliases: [] color: '584774' - description: '' - name: 'Platform: Cordova' - aliases: [] color: '584774' - description: '' - name: 'Platform: Dart' - aliases: [] color: '584774' - description: '' - name: 'Platform: Elixir' - aliases: [] - color: 3268A0 - description: '' + color: '584774' - name: 'Platform: Go' - aliases: [] - color: 2A5DD0 - description: '' + color: '584774' - name: 'Platform: Java' - aliases: [] color: '584774' - description: '' - name: 'Platform: JavaScript' - aliases: [] color: '584774' - description: '' +- name: 'Platform: KMP' + color: '584774' - name: 'Platform: PHP' - aliases: [] color: '584774' - description: '' - name: 'Platform: Python' - aliases: [] color: '584774' - description: '' - name: 'Platform: React-Native' - aliases: [] color: '584774' - description: '' - name: 'Platform: Ruby' - aliases: [] color: '584774' - description: Pull requests that update Ruby code - name: 'Platform: Rust' - aliases: [] color: '584774' - description: '' - name: 'Platform: Symfony' - aliases: [] color: '584774' - description: '' - name: 'Platform: Unity' - aliases: [] color: '584774' - description: '' - name: 'Platform: Unreal' - aliases: [] color: '584774' - description: '' - name: 'Platform: Xamarin' - aliases: [] color: '584774' - description: '' # Waiting for Labels - name: 'Waiting for: Support' @@ -161,19 +75,27 @@ - name: 'Waiting for: Community' color: '8D5494' -# Product Areas - notion.so/sentry/473791bae5bf43399d46093050b77bf0 +# Product Areas - www.notion.so/sentry/473791bae5bf43399d46093050b77bf0 - name: 'Product Area: Unknown' color: '8D5494' - name: 'Product Area: Sign In' color: '8D5494' - name: 'Product Area: Issues' color: '8D5494' +- name: 'Product Area: Issues - Source Maps' + color: '8D5494' - name: 'Product Area: Issues - Suggested Fix' color: '8D5494' - name: 'Product Area: Projects' color: '8D5494' - name: 'Product Area: Performance' color: '8D5494' +- name: 'Product Area: Starfish' + color: '8D5494' +- name: 'Product Area: Starfish - API' + color: '8D5494' +- name: 'Product Area: Starfish - Database' + color: '8D5494' - name: 'Product Area: Profiling' color: '8D5494' - name: 'Product Area: Replays' @@ -214,6 +136,8 @@ color: '8D5494' - name: 'Product Area: Settings - Developer Settings' color: '8D5494' +- name: 'Product Area: Settings - Spend Allocation' + color: '8D5494' - name: 'Product Area: Settings - Spike Protection' color: '8D5494' - name: 'Product Area: Help' @@ -237,188 +161,109 @@ # Teams - name: 'Team: Crons' - aliases: [] color: 8D5494 - description: '' - name: 'Team: Docs' - aliases: [] color: 8D5494 - description: '' - name: 'Team: Ecosystem' - aliases: [] color: 8D5494 - description: '' - name: 'Team: Emerging Technology' - aliases: [] color: 8D5494 description: emerging-technology - name: 'Team: Enterprise' - aliases: [] color: 8D5494 - description: '' - name: 'Team: Front End' - aliases: [] color: 8D5494 description: app-frontend - name: 'Team: Infrastructure' - aliases: [] color: 8D5494 - description: '' - name: 'Team: Ingest' - aliases: [] color: 8D5494 description: owners-ingest - name: 'Team: Mobile Platform' - aliases: [] color: 8D5494 description: team-mobile - name: 'Team: Native Platform' - aliases: [] color: 8D5494 description: owners-native - name: 'Team: Open Source' - aliases: [] color: 8D5494 - description: '' - name: 'Team: Ops' - aliases: [] color: 8D5494 - description: '' - name: 'Team: Replay' - aliases: [] color: 8D5494 description: team-replay - name: 'Team: Revenue' - aliases: [] color: 8D5494 - description: '' - name: 'Team: Search & Storage' - aliases: [] color: 8D5494 description: sns - name: 'Team: Telemetry Experience' - aliases: [] color: 8D5494 description: telemetry experience team - name: 'Team: Visibility' - aliases: [] color: 8D5494 - description: '' - name: 'Team: Web Frontend' - aliases: [] color: 8D5494 description: team-web-sdk-frontend - name: 'Team: Web SDK Backend' - aliases: [] color: 8D5494 description: team-web-sdk-backend - name: 'Team: Workflow' - aliases: [] color: 8D5494 - description: '' # Miscellaneous - name: API Docs - aliases: [] color: 649B42 - description: '' - name: bug - aliases: [] color: F6F6F8 - description: '' - name: Common content - aliases: [] color: B0E299 - description: '' - name: CSS - aliases: [] color: 1279F1 - description: '' - name: duplicate - aliases: [] color: F6F6F8 - description: '' - name: 'Effort: Large' - aliases: [] color: FBCA04 - description: '' - name: 'Effort: Medium' - aliases: [] color: FBCA04 - description: '' - name: 'Effort: Small' - aliases: [] color: FBCA04 - description: '' - name: enhancement - aliases: [] color: F6F6F8 - description: '' - name: filler - aliases: [] color: 57D105 description: Requires little effort to resolve. Ready to be picked up anytime. - name: filtering and sampling - aliases: [] color: '584774' - description: '' - name: gatsby-bug - aliases: [] color: e99695 - description: '' - name: "Hacktoberfest \U0001F383" - aliases: [] color: ffa500 - description: '' - name: Hacktoberfest-accepted - aliases: [] color: e99695 description: Accept for Hacktoberfest - will merge later - name: help wanted - aliases: [] color: F6F6F8 - description: '' - name: Issues - aliases: [] color: D6F6D3 - description: '' - name: javascript - aliases: [] color: F6F6F8 description: Pull requests that update Javascript code - name: Mobile - aliases: [] color: '584774' - description: '' - name: performance - aliases: [] color: '584774' - description: '' - name: up-for-grabs - aliases: [] color: F6F6F8 - description: '' - name: wip - aliases: [] color: F6F6F8 - description: '' - name: wontfix - aliases: [] color: F6F6F8 - description: '' - name: Product Docs - aliases: [] color: F31984 - description: '' - name: projects - aliases: [] color: F6F6F8 - description: '' - name: question - aliases: [] color: F6F6F8 - description: '' - name: release health - aliases: [] color: F6F6F8 - description: '' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index bc092820a5ae0..0000000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: 'close stale issues/PRs' -on: - schedule: - - cron: '0 0 * * *' - workflow_dispatch: -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@87c2b794b9b47a9bec68ae03c01aeb572ffebdb1 - with: - repo-token: ${{ github.token }} - days-before-stale: 21 - days-before-close: 7 - only-labels: "" - operations-per-run: 100 - remove-stale-when-updated: true - debug-only: false - ascending: false - - exempt-issue-labels: "Status: Backlog,Status: In Progress" - stale-issue-label: "Status: Stale" - stale-issue-message: |- - This issue has gone three weeks without activity. In another week, I will close it. - - But! If you comment or otherwise update it, I will reset the clock, and if you label it `Status: Backlog` or `Status: In Progress`, I will leave it alone ... forever! - - ---- - - "A weed is but an unloved flower." ― _Ella Wheeler Wilcox_ πŸ₯€ - skip-stale-issue-message: false - close-issue-label: "" - close-issue-message: "" - - exempt-pr-labels: "Status: Backlog,Status: In Progress" - stale-pr-label: "Status: Stale" - stale-pr-message: |- - This pull request has gone three weeks without activity. In another week, I will close it. - - But! If you comment or otherwise update it, I will reset the clock, and if you label it `Status: Backlog` or `Status: In Progress`, I will leave it alone ... forever! - - ---- - - "A weed is but an unloved flower." ― _Ella Wheeler Wilcox_ πŸ₯€ - skip-stale-pr-message: false - close-pr-label: - close-pr-message: "" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce453459ee24f..ff674d38903a7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: JEKYLL_ENABLE_PLATFORM_API: false steps: - uses: actions/checkout@v2 - - uses: getsentry/action-setup-volta@d6f6ebfc4046feb3cfb6049e885185a01afd82b7 # v1.0.0 + - uses: getsentry/action-setup-volta@54775a59c41065f54ecc76d1dd5f2cdc7a1550cb # v1.1.0 - uses: actions/cache@v2 id: cache with: @@ -48,7 +48,7 @@ jobs: app_id: ${{ vars.SENTRY_INTERNAL_APP_ID }} private_key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }} - - uses: getsentry/action-setup-volta@d6f6ebfc4046feb3cfb6049e885185a01afd82b7 # v1.0.0 + - uses: getsentry/action-setup-volta@54775a59c41065f54ecc76d1dd5f2cdc7a1550cb # v1.1.0 - uses: actions/cache@v2 id: cache @@ -86,7 +86,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: getsentry/action-setup-volta@d6f6ebfc4046feb3cfb6049e885185a01afd82b7 # v1.0.0 + - uses: getsentry/action-setup-volta@54775a59c41065f54ecc76d1dd5f2cdc7a1550cb # v1.1.0 - uses: actions/cache@v2 id: cache with: @@ -102,7 +102,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: getsentry/action-setup-volta@d6f6ebfc4046feb3cfb6049e885185a01afd82b7 # v1.0.0 + - uses: getsentry/action-setup-volta@54775a59c41065f54ecc76d1dd5f2cdc7a1550cb # v1.1.0 - uses: actions/cache@v2 id: cache with: diff --git a/LICENSE b/LICENSE index c8594cdecde90..8759a6b6a96e4 100644 --- a/LICENSE +++ b/LICENSE @@ -16,7 +16,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that you d error-reporting or application monitoring features of the Licensed Work. -Change Date: 2026-06-15 +Change Date: 2026-07-25 Change License: Apache License, Version 2.0 diff --git a/__mocks__/@reach/router.js b/__mocks__/@reach/router.js index 05669f237f4eb..0c403e888de4b 100644 --- a/__mocks__/@reach/router.js +++ b/__mocks__/@reach/router.js @@ -1,5 +1,4 @@ module.exports = { ...jest.requireActual('@gatsbyjs/reach-router'), useLocation: jest.fn(), - useNavigate: jest.fn(() => jest.fn()), }; diff --git a/__mocks__/gatsby.js b/__mocks__/gatsby.js index ab1bac4a06e41..e236ebe193417 100644 --- a/__mocks__/gatsby.js +++ b/__mocks__/gatsby.js @@ -1,10 +1,11 @@ /* eslint-disable no-unused-vars */ -const React = require("react"); +const React = require('react'); module.exports = { - ...jest.requireActual("gatsby"), + ...jest.requireActual('gatsby'), graphql: jest.fn(), + navigate: jest.fn(), Link: jest.fn().mockImplementation( // these props are invalid for an `a` tag ({ @@ -18,7 +19,7 @@ module.exports = { to, ...rest }) => - React.createElement("a", { + React.createElement('a', { ...rest, href: to, }) diff --git a/bin/lint-docs.ts b/bin/lint-docs.ts index 0e997e6ce29fb..087014e46186d 100755 --- a/bin/lint-docs.ts +++ b/bin/lint-docs.ts @@ -1,6 +1,11 @@ #!/usr/bin/env ts-node -import fs from "fs"; -import PlatformRegistry from "../src/shared/platformRegistry"; + +/* eslint-env node */ +/* eslint import/no-nodejs-modules:0 */ + +import fs from 'fs'; + +import {buildPlatformRegistry} from '../src/shared/platformRegistry'; enum Level { error, @@ -8,12 +13,12 @@ enum Level { } type Violation = { - level?: Level; message: string; - context: string; + context?: string; + level?: Level; }; -const hasWizardContent = (platformName: string, guideName = null) => { +const hasWizardContent = (platformName: string, guideName: string | null = null) => { const path = guideName ? `src/wizard/${platformName}/${guideName}.md` : `src/wizard/${platformName}/index.md`; @@ -26,29 +31,27 @@ const hasWizardContent = (platformName: string, guideName = null) => { }; const testConfig = config => { - const violations = []; + const violations: Violation[] = []; if (!config.title) { - violations.push({ level: Level.error, message: `Missing title` }); + violations.push({level: Level.error, message: `Missing title`}); } if (!config.caseStyle) { - violations.push({ level: Level.warn, message: `Missing caseStyle` }); + violations.push({level: Level.warn, message: `Missing caseStyle`}); } if (!config.supportLevel) { - violations.push({ level: Level.warn, message: `Missing supportLevel` }); + violations.push({level: Level.warn, message: `Missing supportLevel`}); } return violations; }; const main = async () => { - const platformRegistry = new PlatformRegistry(); - await platformRegistry.init(); - + const {platforms} = await buildPlatformRegistry(); const violations: Violation[] = []; - platformRegistry.platforms.forEach(platform => { + platforms.forEach(platform => { // test for wizard testConfig(platform).forEach(violation => { - violations.push({ ...violation, context: platform.key }); + violations.push({...violation, context: platform.key}); }); if (!hasWizardContent(platform.name)) { violations.push({ @@ -59,7 +62,7 @@ const main = async () => { } platform.guides.forEach(guide => { testConfig(platform).forEach(violation => { - violations.push({ ...violation, context: guide.key }); + violations.push({...violation, context: guide.key}); }); if (!hasWizardContent(platform.name, guide.name)) { violations.push({ @@ -72,18 +75,21 @@ const main = async () => { }); let numErrors = 0; - violations.forEach(({ level, message, context }) => { + violations.forEach(({level, message, context}) => { if (level === Level.error) { numErrors += 1; } switch (level) { case Level.error: + // eslint-disable-next-line no-console console.error(`ERROR: ${message} in ${context}`); break; case Level.warn: + // eslint-disable-next-line no-console console.warn(`WARN: ${message} in ${context}`); break; default: + // eslint-disable-next-line no-console console.info(`INFO: ${message} in ${context}`); break; } @@ -93,6 +99,7 @@ const main = async () => { main() .catch(err => { + // eslint-disable-next-line no-console console.error(err); process.exit(1); }) diff --git a/gatsby-browser.js b/gatsby-browser.tsx similarity index 50% rename from gatsby-browser.js rename to gatsby-browser.tsx index 1a043e96420b0..7a9b945f6e4c2 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.tsx @@ -1,15 +1,15 @@ -/* eslint-env node */ -/* eslint import/no-nodejs-modules:0 */ - import React from 'react'; +import {GatsbyBrowser} from 'gatsby'; + import PageContext from 'sentry-docs/components/pageContext'; -export const wrapPageElement = ({element, props: {pageContext}}) => ( - {element} -); +export const wrapPageElement: GatsbyBrowser['wrapPageElement'] = ({ + element, + props: {pageContext}, +}) => {element}; // Disable prefetching altogether so our bw is not destroyed. // If this turns out to hurt performance significantly, we can // try out https://www.npmjs.com/package/gatsby-plugin-guess-js // with data from the prior 1-2 weeks. -export const disableCorePrefetching = () => true; +export const disableCorePrefetching: GatsbyBrowser['disableCorePrefetching'] = () => true; diff --git a/gatsby-ssr.js b/gatsby-ssr.tsx similarity index 81% rename from gatsby-ssr.js rename to gatsby-ssr.tsx index 08c5a6cc99ef6..eb89ef36027cc 100644 --- a/gatsby-ssr.js +++ b/gatsby-ssr.tsx @@ -2,22 +2,25 @@ /* eslint import/no-nodejs-modules:0 */ import React from 'react'; +import {GatsbySSR} from 'gatsby'; import {PageContext} from 'sentry-docs/components/pageContext'; const sentryEnvironment = process.env.GATSBY_ENV || process.env.NODE_ENV || 'development'; const sentryLoaderUrl = process.env.SENTRY_LOADER_URL; -export const wrapPageElement = ({element, props: {pageContext}}) => ( - {element} -); +export const wrapPageElement: GatsbySSR['wrapPageElement'] = ({ + element, + props: {pageContext}, +}) => {element}; -export const onPreRenderHTML = ({getHeadComponents}) => { +export const onPreRenderHTML: GatsbySSR['onPreRenderHTML'] = ({getHeadComponents}) => { if (process.env.NODE_ENV !== 'production') { return; } - getHeadComponents().forEach(el => { + // TODO(epurkhiser): We should figure out if this is actually still necessary + getHeadComponents().forEach((el: any) => { // Remove inline css. https://github.com/gatsbyjs/gatsby/issues/1526 if (el.type === 'style') { el.type = 'link'; @@ -61,7 +64,7 @@ Sentry.onLoad(function() { ); } -export const onRenderBody = ({setHeadComponents}) => { +export const onRenderBody: GatsbySSR['onRenderBody'] = ({setHeadComponents}) => { // Sentry SDK setup if (sentryLoaderUrl) { setHeadComponents([SentryLoaderScript(), SentryLoaderConfig()]); diff --git a/package.json b/package.json index c14b56913cf16..9c3110dd99972 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,12 @@ "@sentry/browser": "7.55.2", "@sentry/webpack-plugin": "2.2.2", "@types/dompurify": "^3.0.2", + "@types/js-cookie": "^3.0.3", + "@types/jest": "^29.5.3", + "@types/js-yaml": "^3.0.0", "@types/node": "^20.3.1", + "@types/react": "18.2.12", + "@types/react-dom": "18.2.5", "@types/react-helmet": "^6.1.0", "@typescript-eslint/eslint-plugin": "^5.59.11", "@typescript-eslint/parser": "^5.59.11", @@ -29,35 +34,37 @@ "crypto-browserify": "^3.12.0", "dompurify": "^3.0.3", "dotenv": "^16.1.4", - "framer-motion": "^3.3.0", + "framer-motion": "^10.12.16", "gatsby": "^4.25.7", "gatsby-cli": "^4.0.0", "gatsby-plugin-algolia": "^0.11.2", "gatsby-plugin-mdx": "^3.0.0", "gatsby-plugin-meta-redirect": "^1.1.1", "gatsby-plugin-react-helmet": "^3.3.10", - "gatsby-plugin-sass": "^5.0.0", + "gatsby-plugin-sass": "^6.10.0", "gatsby-plugin-sharp": "^4.25.1", - "gatsby-plugin-sitemap": "^5.20.0", + "gatsby-plugin-sitemap": "^6.10.0", "gatsby-plugin-zeit-now": "^0.3.0", - "gatsby-remark-autolink-headers": "^5.0.0", + "gatsby-remark-autolink-headers": "^6.10.0", "gatsby-remark-check-links": "^2.1.0", "gatsby-remark-copy-linked-files": "^5.0.0", - "gatsby-remark-images": "^6.0.0", - "gatsby-remark-prismjs": "^6.0.0", + "gatsby-remark-images": "^7.10.0", + "gatsby-remark-prismjs": "^7.10.0", "gatsby-source-filesystem": "^4.0.0", "gatsby-transformer-javascript-frontmatter": "^4.0.0", "gatsby-transformer-json": "^4.0.0", "gatsby-transformer-remark": "^5.0.0", "gray-matter": "^4.0.2", + "js-cookie": "^3.0.5", + "js-yaml": "^3.0.0", "jsdom": "^22.1.0", "platformicons": "^5.6.0", "prism-sentry": "^1.0.2", "prismjs": "^1.27.0", "query-string": "^6.13.1", - "react": "^17.0.2", - "react-bootstrap": "^1.3.0", - "react-dom": "^17.0.2", + "react": "^18.2.0", + "react-bootstrap": "^1.6.7", + "react-dom": "^18.2.0", "react-feather": "^2.0.8", "react-helmet": "^5.2.1", "react-popper": "^2.2.4", @@ -70,13 +77,9 @@ "typescript": "^5.1.3" }, "resolutions": { - "@types/react": "17.0.39", - "@types/react-dom": "17.0.11", "@typescript-eslint/eslint-plugin": "^5.59.11", "@typescript-eslint/parser": "^5.59.11", - "set-value": ">=4.0.1", - "trim": ">=1.0.1", - "xmlhttprequest-ssl": ">=1.6.2" + "trim": ">=1.0.1" }, "devDependencies": { "@testing-library/react-hooks": "^8.0.1", @@ -88,7 +91,7 @@ "jest-dom": "^4.0.0", "jest-environment-jsdom": "^29.5.0", "prettier": "^2.8.7", - "react-test-renderer": "^17.0.0", + "react-test-renderer": "^18.2.0", "ts-jest": "^29.1.0" }, "scripts": { diff --git a/scripts/remove-preview-deployments.ts b/scripts/remove-preview-deployments.ts index da5dc5c8d2980..9b440a0bd36c5 100644 --- a/scripts/remove-preview-deployments.ts +++ b/scripts/remove-preview-deployments.ts @@ -32,64 +32,116 @@ if (!VERCEL_PROJECT_ID) { throw new Error('VERCEL_PROJECT_ID is not set'); } -/** This object contains information related to the pagination of the current request, including the necessary parameters to get the next or previous page of data. */ +/** + * This object contains information related to the pagination of the current + * request, including the necessary parameters to get the next or previous page + * of data. + */ interface Pagination { - /** Amount of items in the current page. */ + /** + * Amount of items in the current page. + */ count: number; - /** Timestamp that must be used to request the next page. */ + /** + * Timestamp that must be used to request the next page. + */ next: number | null; - /** Timestamp that must be used to request the previous page. */ + /** + * Timestamp that must be used to request the previous page. + */ prev: number | null; } interface Response { deployments: { - /** Timestamp of when the deployment got created. */ + /** + * Timestamp of when the deployment got created. + * */ created: number; - /** Metadata information of the user who created the deployment. */ + /** + * Metadata information of the user who created the deployment. + */ creator: { - /** The unique identifier of the user. */ + /** + * The unique identifier of the user. + */ uid: string; - /** The email address of the user. */ + /** + * The email address of the user. + */ email?: string; - /** The GitHub login of the user. */ + /** + * The GitHub login of the user. + */ githubLogin?: string; - /** The GitLab login of the user. */ + /** + * The GitLab login of the user. + */ gitlabLogin?: string; - /** The username of the user. */ + /** + * The username of the user. + */ username?: string; }; - /** Vercel URL to inspect the deployment. */ + /** + * Vercel URL to inspect the deployment. + */ inspectorUrl: string | null; - /** The name of the deployment. */ + /** + * The name of the deployment. + */ name: string; - /** The type of the deployment. */ + /** + * The type of the deployment. + */ type: 'LAMBDAS'; - /** The unique identifier of the deployment. */ + /** + * The unique identifier of the deployment. + */ uid: string; - /** The URL of the deployment. */ + /** + * The URL of the deployment. + */ url: string; aliasAssigned?: (number | boolean) | null; - /** An error object in case aliasing of the deployment failed. */ + /** + * An error object in case aliasing of the deployment failed. + */ aliasError?: { code: string; message: string; } | null; - /** Timestamp of when the deployment started building at. */ + /** + * Timestamp of when the deployment started building at. + */ buildingAt?: number; - /** Conclusion for checks */ + /** + * Conclusion for checks + */ checksConclusion?: 'succeeded' | 'failed' | 'skipped' | 'canceled'; - /** State of all registered checks */ + /** + * State of all registered checks + */ checksState?: 'registered' | 'running' | 'completed'; - /** The ID of Vercel Connect configuration used for this deployment */ + /** + * The ID of Vercel Connect configuration used for this deployment + */ connectConfigurationId?: string; - /** Timestamp of when the deployment got created. */ + /** + * Timestamp of when the deployment got created. + */ createdAt?: number; - /** Deployment can be used for instant rollback */ + /** + * Deployment can be used for instant rollback + */ isRollbackCandidate?: boolean | null; - /** An object containing the deployment's metadata */ + /** + * An object containing the deployment's metadata + */ meta?: {[key: string]: string}; - /** The project settings which was used for this deployment */ + /** + * The project settings which was used for this deployment + */ projectSettings?: { buildCommand?: string | null; commandForIgnoringBuildStep?: string | null; @@ -151,13 +203,21 @@ interface Response { skipGitConnectDuringLink?: boolean; sourceFilesOutsideRootDirectory?: boolean; }; - /** Timestamp of when the deployment got ready. */ + /** + * Timestamp of when the deployment got ready. + */ ready?: number; - /** The source of the deployment. */ + /** + * The source of the deployment. + */ source?: 'cli' | 'git' | 'import' | 'import/repo' | 'clone/repo'; - /** In which state is the deployment. */ + /** + * In which state is the deployment. + */ state?: 'BUILDING' | 'ERROR' | 'INITIALIZING' | 'QUEUED' | 'READY' | 'CANCELED'; - /** On which environment has the deployment been deployed to. */ + /** + * On which environment has the deployment been deployed to. + */ target?: ('production' | 'staging') | null; }[]; pagination: Pagination; @@ -226,7 +286,11 @@ async function run(next: number): Promise { console.log(`Waiting for ${Math.round(timeout / 1000)} seconds`); await new Promise(resolve => setTimeout(resolve, timeout)); - run(rateLimit ? next : pagination.next); + const nextPage = rateLimit ? next : pagination.next; + + if (nextPage) { + run(nextPage); + } } // Start the script with a date 30 days ago diff --git a/src/api/auth.mdx b/src/api/auth.mdx index 3847a4e7c6160..8c058fb5c1708 100644 --- a/src/api/auth.mdx +++ b/src/api/auth.mdx @@ -23,7 +23,7 @@ You can create authentication tokens within Sentry by [creating an internal inte ### User authentication tokens -Some API endpoints require an authentication token that's associated with your user account, rather than an authentication token from an internal integration. These auth tokens can be created within Sentry on the "User settings" page (**User settings > Auth Tokens**) and assigned specific scopes. +Some API endpoints require an authentication token that's associated with your user account, rather than an authentication token from an internal integration. These auth tokens can be created within Sentry on the "User settings" page (**User settings > User Auth Tokens**) and assigned specific scopes. The endpoints that require a user authentication token are specific to your user, such as [List Your Organizations](/api/organizations/list-your-organizations/). diff --git a/src/api/index.mdx b/src/api/index.mdx index 839c4d7503563..9c4102ab8b8a0 100644 --- a/src/api/index.mdx +++ b/src/api/index.mdx @@ -2,14 +2,17 @@ title: API Reference --- -The Sentry API is used for submitting events to the Sentry collector as well as exporting and managing data. The reporting and web APIs are individually versioned. This document refers to the web APIs only. For information about the reporting API see [_SDK Development_](https://develop.sentry.dev/sdk/overview/). +The Sentry web API is used to access the Sentry platform programmatically. You can use the APIs to manage account-level resources, like organizations and teams, as well as manage and export data. + +If you're looking for information about the API surface for Sentry's SDKs, see the [SDK Development](https://develop.sentry.dev/sdk/overview/) docs. ## Versioning -The current version of the web API is known as **v0** and is considered to be in a draft phase. While we don’t expect public endpoints to change greatly, keep in mind that the API is still under development. +Sentry's web API is **v0** and under development. Public endpoints, especially those marked as beta may change. ## Getting Started +- [Sentry Postman Collection](https://www.postman.com/sentryio) - [Authentication](/api/auth/) - [Pagination](/api/pagination/) - [Permissions](/api/permissions/) diff --git a/src/api/ratelimits.mdx b/src/api/ratelimits.mdx index 042784642fe1d..372822b2e6638 100644 --- a/src/api/ratelimits.mdx +++ b/src/api/ratelimits.mdx @@ -7,7 +7,7 @@ Sentry rate limits every API request made to prevent abuse and resource overuse. We restrict both how frequently a request is made (requests per second rate limit) and how many concurrent requests a caller can make (concurrent rate limit). -The requests per second rate limit follows a fixed window approach. Requests are counted into time buckets, determined by the window size. If the number of requests from a caller exceeds the number of allowed requests within a window, then that request will be rejected. While there is a default rate limit of **40 requests per second**, each endpoint can have their own maximum number of requests and window size. +The requests per second rate limit follows a fixed window approach. Requests are counted into time buckets, determined by the window size. If the number of requests from a caller exceeds the number of allowed requests within a window, then that request will be rejected. Each endpoint has its own maximum number of requests and window size. Meanwhile, the concurrent rate limiter will reject requests if the caller has too many requests in progress at the same time. diff --git a/src/components/__tests__/__snapshots__/codeBlock.test.js.snap b/src/components/__tests__/__snapshots__/codeBlock.test.js.snap new file mode 100644 index 0000000000000..5a9974eb4eb26 --- /dev/null +++ b/src/components/__tests__/__snapshots__/codeBlock.test.js.snap @@ -0,0 +1,250 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CodeWrapper renders multiple placeholders 1`] = ` + + process.env.MY_ENV = + + + + + + + + example-org + + + + + + + + + + + + + example-project + + + + +`; + +exports[`CodeWrapper renders org auth token placeholder when not signed in 1`] = ` + + process.env.MY_ENV = + sntrys_YOUR_TOKEN_HERE + +`; + +exports[`CodeWrapper renders org auth token placeholder when signed in 1`] = ` + + process.env.MY_ENV = + + Click to generate token + + +`; + +exports[`CodeWrapper renders with placeholder 1`] = ` + + process.env.MY_ENV = + + + + + + + + example-org + + + + +`; + +exports[`CodeWrapper renders with placeholder in middle of text 1`] = ` + + process.env.MY_ENV = https:// + + + + + + + + example-org + + + + .sentry.io + +`; + +exports[`CodeWrapper renders without placeholder 1`] = ` + + process.env.MY_ENV = 'foo' + +`; diff --git a/src/components/__tests__/__snapshots__/configKey.test.js.snap b/src/components/__tests__/__snapshots__/configKey.test.js.snap index 282f3cfc026de..6fb271edaf454 100644 --- a/src/components/__tests__/__snapshots__/configKey.test.js.snap +++ b/src/components/__tests__/__snapshots__/configKey.test.js.snap @@ -19,6 +19,8 @@ exports[`ConfigKey renders correctly 1`] = ` /> - + + my_option_name + `; diff --git a/src/components/__tests__/codeBlock.test.js b/src/components/__tests__/codeBlock.test.js new file mode 100644 index 0000000000000..0d6f5d39fd0c1 --- /dev/null +++ b/src/components/__tests__/codeBlock.test.js @@ -0,0 +1,74 @@ +import React from 'react'; +import {create} from 'react-test-renderer'; + +import {CodeWrapper} from '../codeBlock'; +import {_setCachedCodeKeywords, CodeContextProvider, DEFAULTS} from '../codeContext'; + +describe('CodeWrapper', () => { + beforeEach(() => { + _setCachedCodeKeywords(DEFAULTS); + }); + + it('renders without placeholder', () => { + const tree = create( + + process.env.MY_ENV = 'foo' + + ); + + expect(tree.toJSON()).toMatchSnapshot(); + }); + + it('renders with placeholder', () => { + const tree = create( + + process.env.MY_ENV = ___ORG_SLUG___ + + ); + + expect(tree.toJSON()).toMatchSnapshot(); + }); + + it('renders with placeholder in middle of text', () => { + const tree = create( + + process.env.MY_ENV = https://___ORG_SLUG___.sentry.io + + ); + + expect(tree.toJSON()).toMatchSnapshot(); + }); + + it('renders org auth token placeholder when not signed in', () => { + const tree = create( + + process.env.MY_ENV = ___ORG_AUTH_TOKEN___ + + ); + + expect(tree.toJSON()).toMatchSnapshot(); + }); + it('renders org auth token placeholder when signed in', () => { + _setCachedCodeKeywords({...DEFAULTS, USER: {ID: 123, NAME: 'test@sentry.io'}}); + + const tree = create( + + process.env.MY_ENV = ___ORG_AUTH_TOKEN___ + + ); + + expect(tree.toJSON()).toMatchSnapshot(); + }); + + it('renders multiple placeholders', () => { + const tree = create( + + + process.env.MY_ENV = ___ORG_SLUG___ + ___PROJECT_SLUG___ + + + ); + + expect(tree.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/src/components/__tests__/tableOfContents.test.tsx b/src/components/__tests__/tableOfContents.test.tsx new file mode 100644 index 0000000000000..de463a90d0b7a --- /dev/null +++ b/src/components/__tests__/tableOfContents.test.tsx @@ -0,0 +1,157 @@ +import {buildTocTree} from '../tableOfContents'; + +describe('buildTocTree', function () { + it('constructs a simple TOC tree', function () { + const headings = [ + { + id: 'item-1', + title: 'Item 1', + level: 1, + }, + { + id: 'item-2', + title: 'Item 2', + level: 2, + }, + { + id: 'item-3', + title: 'Item 3', + level: 3, + }, + { + id: 'item-4', + title: 'Item 4', + level: 2, + }, + { + id: 'item-5', + title: 'Item 5', + level: 1, + }, + ]; + + const expectedTree = [ + { + id: 'item-1', + title: 'Item 1', + level: 1, + items: [ + { + id: 'item-2', + title: 'Item 2', + level: 2, + items: [ + { + id: 'item-3', + title: 'Item 3', + level: 3, + items: [], + }, + ], + }, + { + id: 'item-4', + title: 'Item 4', + level: 2, + items: [], + }, + ], + }, + { + id: 'item-5', + title: 'Item 5', + level: 1, + items: [], + }, + ]; + + expect(buildTocTree(headings)).toEqual(expectedTree); + }); + + it('handles out of order items', function () { + const headings = [ + { + id: 'item-1', + title: 'Item 1', + level: 3, + }, + { + id: 'item-2', + title: 'Item 2', + level: 2, + }, + { + id: 'item-3', + title: 'Item 3', + level: 1, + }, + ]; + + const expectedTree = [ + { + id: 'item-1', + title: 'Item 1', + level: 3, + items: [], + }, + { + id: 'item-2', + title: 'Item 2', + level: 2, + items: [], + }, + { + id: 'item-3', + title: 'Item 3', + level: 1, + items: [], + }, + ]; + + expect(buildTocTree(headings)).toEqual(expectedTree); + }); + + it('handles skipping levels', function () { + const headings = [ + { + id: 'item-1', + title: 'Item 1', + level: 1, + }, + { + id: 'item-2', + title: 'Item 2', + level: 3, + }, + { + id: 'item-3', + title: 'Item 3', + level: 2, + }, + ]; + + const expectedTree = [ + { + id: 'item-1', + title: 'Item 1', + level: 1, + items: [ + { + id: 'item-2', + title: 'Item 2', + level: 3, + items: [], + }, + { + id: 'item-3', + title: 'Item 3', + level: 2, + items: [], + }, + ], + }, + ]; + + expect(buildTocTree(headings)).toEqual(expectedTree); + }); +}); diff --git a/src/components/alert.tsx b/src/components/alert.tsx index ec152a53e9406..226de65a8f4ad 100644 --- a/src/components/alert.tsx +++ b/src/components/alert.tsx @@ -8,13 +8,7 @@ type Props = { title?: string; }; -export function Alert({ - title, - children, - level, - deepLink, - dismiss = false, -}: Props): JSX.Element { +export function Alert({title, children, level, deepLink, dismiss = false}: Props) { let className = 'alert'; if (level) { className += ` alert-${level}`; diff --git a/src/components/banner.tsx b/src/components/banner.tsx index 217a4f764d930..7aba523a33352 100644 --- a/src/components/banner.tsx +++ b/src/components/banner.tsx @@ -6,11 +6,9 @@ import React, {useEffect, useState} from 'react'; // we put a more robust solution in place. // const SHOW_BANNER = false; -const BANNER_TEXT = - 'Join us on May 30th at 10am PT for a 20-minute session on frontend error monitoring with Sentry.'; -const BANNER_LINK_URL = - 'https://sentry.io/resources/livestream-ama-frontend-error-monitoring-101/?utm_medium=banner&utm_source=sentry-app&utm_campaign=frontend-error-webinar-may&utm_content=docs-banner'; -const BANNER_LINK_TEXT = 'Register now.'; +const BANNER_TEXT = ''; +const BANNER_LINK_URL = ''; +const BANNER_LINK_TEXT = ''; const OPTIONAL_BANNER_IMAGE = null; // diff --git a/src/components/basePage.tsx b/src/components/basePage.tsx index 0edcc7ec10c04..54f6ecd9ce404 100644 --- a/src/components/basePage.tsx +++ b/src/components/basePage.tsx @@ -1,9 +1,9 @@ -import React, {forwardRef, Fragment, useRef} from 'react'; +import React, {Fragment, useState} from 'react'; import {getCurrentTransaction} from '../utils'; import {Banner} from './banner'; -import {CodeContext, useCodeContextState} from './codeContext'; +import {CodeContextProvider} from './codeContext'; import {GitHubCTA} from './githubCta'; import {Layout} from './layout'; import {SEO} from './seo'; @@ -20,18 +20,8 @@ export type PageContext = { title?: string; }; -type WrappedTOCProps = { - pageContext: PageContext; -}; - -const WrappedTOC = forwardRef( - (props: WrappedTOCProps, ref: React.RefObject) => { - return ; - } -); - type Props = { - children?: JSX.Element; + children?: React.ReactNode; data?: { file?: { [key: string]: any; @@ -44,9 +34,9 @@ type Props = { notoc?: boolean; title?: string; }; - prependToc?: JSX.Element; + prependToc?: React.ReactNode; seoTitle?: string; - sidebar?: JSX.Element; + sidebar?: React.ReactNode; }; export function BasePage({ @@ -56,7 +46,7 @@ export function BasePage({ sidebar, children, prependToc, -}: Props): JSX.Element { +}: Props) { const tx = getCurrentTransaction(); if (tx) { tx.setStatus('ok'); @@ -65,7 +55,7 @@ export function BasePage({ const {title, excerpt, description} = pageContext; const hasToc = !pageContext.notoc; - const contentRef = useRef(null); + const [contentElement, setContentElement] = useState(null); const pageDescription = description || (excerpt ? excerpt.slice(0, 160) : ''); @@ -86,10 +76,8 @@ export function BasePage({ } >

{title}

-
- - {children} - +
+ {children} {file && ( {prependToc} - {hasToc && } + {hasToc && contentElement && ( + + )}
diff --git a/src/components/breadcrumbs.tsx b/src/components/breadcrumbs.tsx index 10c37bff666ec..8b9601b5a28b2 100644 --- a/src/components/breadcrumbs.tsx +++ b/src/components/breadcrumbs.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {useLocation} from '@reach/router'; -import {graphql, StaticQuery} from 'gatsby'; +import {graphql, useStaticQuery} from 'gatsby'; import {SmartLink} from './smartLink'; @@ -79,13 +79,8 @@ export function BaseBreadcrumbs({ ); } -export function breadcrumb() { - return ( - { - return ; - }} - /> - ); +export function Breadcrumbs() { + const data = useStaticQuery(query); + + return ; } diff --git a/src/components/cliChecksumTable.tsx b/src/components/cliChecksumTable.tsx index 1ae74d1f93f4b..934d810dd7d80 100644 --- a/src/components/cliChecksumTable.tsx +++ b/src/components/cliChecksumTable.tsx @@ -22,7 +22,7 @@ const ChecksumValue = styled.code` white-space: nowrap; `; -export function CliChecksumTable(): JSX.Element { +export function CliChecksumTable() { const { app: {files, version}, } = useStaticQuery(query); diff --git a/src/components/codeBlock.tsx b/src/components/codeBlock.tsx index 9fc051c2b359e..8ff63ca19c2ff 100644 --- a/src/components/codeBlock.tsx +++ b/src/components/codeBlock.tsx @@ -9,48 +9,80 @@ import memoize from 'lodash/memoize'; import {useOnClickOutside} from 'sentry-docs/utils'; -import {CodeContext} from './codeContext'; +import {CodeContext, createOrgAuthToken} from './codeContext'; const KEYWORDS_REGEX = /\b___(?:([A-Z_][A-Z0-9_]*)\.)?([A-Z_][A-Z0-9_]*)___\b/g; +const ORG_AUTH_TOKEN_REGEX = /___ORG_AUTH_TOKEN___/g; + +type ChildrenItem = ReturnType[number] | React.ReactNode; + function makeKeywordsClickable(children: React.ReactNode) { const items = Children.toArray(children); - KEYWORDS_REGEX.lastIndex = 0; - - return items.reduce((arr: any[], child) => { + return items.reduce((arr: ChildrenItem[], child) => { if (typeof child !== 'string') { arr.push(child); return arr; } - let match; - let lastIndex = 0; - // eslint-disable-next-line no-cond-assign - while ((match = KEYWORDS_REGEX.exec(child)) !== null) { - const afterMatch = KEYWORDS_REGEX.lastIndex - match[0].length; - const before = child.substring(lastIndex, afterMatch); - if (before.length > 0) { - arr.push(before); - } - arr.push( - - ); - lastIndex = KEYWORDS_REGEX.lastIndex; + if (ORG_AUTH_TOKEN_REGEX.test(child)) { + makeOrgAuthTokenClickable(arr, child); + } else if (KEYWORDS_REGEX.test(child)) { + makeProjectKeywordsClickable(arr, child); + } else { + arr.push(child); } - const after = child.substring(lastIndex); - if (after.length > 0) { - arr.push(after); + return arr; + }, [] as ChildrenItem[]); +} + +function makeOrgAuthTokenClickable(arr: ChildrenItem[], str: string) { + runRegex(arr, str, ORG_AUTH_TOKEN_REGEX, lastIndex => ( + + )); +} + +function makeProjectKeywordsClickable(arr: ChildrenItem[], str: string) { + runRegex(arr, str, KEYWORDS_REGEX, (lastIndex, match) => ( + + )); +} + +function runRegex( + arr: ChildrenItem[], + str: string, + regex: RegExp, + cb: (lastIndex: number, match: any[]) => React.ReactNode +): void { + regex.lastIndex = 0; + + let match; + let lastIndex = 0; + // eslint-disable-next-line no-cond-assign + while ((match = regex.exec(str)) !== null) { + const afterMatch = regex.lastIndex - match[0].length; + const before = str.substring(lastIndex, afterMatch); + + if (before.length > 0) { + arr.push(before); } - return arr; - }, []); + arr.push(cb(lastIndex, match)); + + lastIndex = regex.lastIndex; + } + + const after = str.substring(lastIndex); + if (after.length > 0) { + arr.push(after); + } } const getPortal = memoize((): HTMLElement => { @@ -73,6 +105,57 @@ type KeywordSelectorProps = { keyword: string; }; +function OrgAuthTokenCreator() { + const codeContext = useContext(CodeContext); + + const [tokenState, setTokenState] = useState<'none' | 'loading' | 'success' | 'error'>( + 'none' + ); + const [token, setToken] = useState(null); + const [sharedSelection] = codeContext.sharedKeywordSelection; + + const {codeKeywords} = useContext(CodeContext); + + const choices = codeKeywords?.PROJECT; + + // When not signed in, we just show a placeholder, as the user can't generate a token in this case + if (!codeKeywords.USER) { + return sntrys_YOUR_TOKEN_HERE; + } + + const currentSelectionIdx = sharedSelection.PROJECT ?? 0; + const currentSelection = choices[currentSelectionIdx]; + + const name = `Generated by Docs for ${currentSelection.PROJECT_SLUG} on ${new Date() + .toISOString() + .slice(0, 10)}`; + + const updateToken = async () => { + if (tokenState !== 'none') { + return; + } + setTokenState('loading'); + const tokenStr = await createOrgAuthToken({ + orgSlug: currentSelection.ORG_SLUG, + name, + }); + setTokenState(token ? 'success' : 'error'); + setToken(tokenStr); + }; + + return ( + + {tokenState === 'none' + ? 'Click to generate token' + : tokenState === 'loading' + ? 'Generating...' + : token + ? token + : 'Error generating token'} + + ); +} + function KeywordSelector({keyword, group, index}: KeywordSelectorProps) { const codeContext = useContext(CodeContext); @@ -214,7 +297,9 @@ const KeywordDropdown = styled('span')` } `; -const KeywordIndicator = styled(ArrowDown)<{isOpen: boolean}>` +const KeywordIndicator = styled(ArrowDown, {shouldForwardProp: p => p !== 'isOpen'})<{ + isOpen: boolean; +}>` user-select: none; margin-right: 2px; transition: transform 200ms ease-in-out; @@ -317,7 +402,7 @@ const ItemButton = styled('button')<{isActive: boolean}>` `} `; -function CodeWrapper(props): JSX.Element { +export function CodeWrapper(props) { const {children, class: className, ...rest} = props; return ( @@ -327,7 +412,7 @@ function CodeWrapper(props): JSX.Element { ); } -function SpanWrapper(props): JSX.Element { +function SpanWrapper(props) { const {children, class: className, ...rest} = props; return ( @@ -337,13 +422,13 @@ function SpanWrapper(props): JSX.Element { } type Props = { - children: JSX.Element; + children: React.ReactNode; filename?: string; language?: string; title?: string; }; -export function CodeBlock({filename, language, children}: Props): JSX.Element { +export function CodeBlock({filename, language, children}: Props) { const [showCopied, setShowCopied] = useState(false); const codeRef = useRef(null); diff --git a/src/components/codeContext.tsx b/src/components/codeContext.tsx index d617da512fe33..b587835ec67a4 100644 --- a/src/components/codeContext.tsx +++ b/src/components/codeContext.tsx @@ -1,4 +1,5 @@ -import {createContext, useEffect, useState} from 'react'; +import React, {createContext, useEffect, useState} from 'react'; +import Cookies from 'js-cookie'; type ProjectCodeKeywords = { API_URL: string; @@ -16,8 +17,14 @@ type ProjectCodeKeywords = { title: string; }; +type UserCodeKeywords = { + ID: number; + NAME: string; +}; + type CodeKeywords = { PROJECT: ProjectCodeKeywords[]; + USER: UserCodeKeywords | undefined; }; type Dsn = { @@ -31,17 +38,28 @@ type Dsn = { type ProjectApiResult = { dsn: string; dsnPublic: string; - id: string; - organizationId: string; + id: number; + name: string; + organizationId: number; + organizationName: string; organizationSlug: string; + projectName: string; projectSlug: string; - slug: string; + publicKey: string; + secretKey: string; +}; + +type UserApiResult = { + avatarUrl: string; + id: number; + isAuthenticated: boolean; + name: string; }; // only fetch them once -let cachedCodeKeywords = null; +let cachedCodeKeywords: CodeKeywords | null = null; -const DEFAULTS: CodeKeywords = { +export const DEFAULTS: CodeKeywords = { PROJECT: [ { DSN: 'https://examplePublicKey@o0.ingest.sentry.io/0', @@ -60,19 +78,28 @@ const DEFAULTS: CodeKeywords = { title: `example-org / example-project`, }, ], + USER: undefined, }; type CodeContextType = { codeKeywords: CodeKeywords; - sharedCodeSelection: any; - sharedKeywordSelection: any; + isLoading: boolean; + sharedCodeSelection: [string | null, React.Dispatch]; + sharedKeywordSelection: [ + Record, + React.Dispatch> + ]; }; export const CodeContext = createContext(null); -const parseDsn = function (dsn: string): Dsn { +function parseDsn(dsn: string): Dsn { const match = dsn.match(/^(.*?\/\/)(.*?):(.*?)@(.*?)(\/.*?)$/); + if (match === null) { + throw new Error('Failed to parse DSN'); + } + return { scheme: match[1], publicKey: escape(match[2]), @@ -80,7 +107,7 @@ const parseDsn = function (dsn: string): Dsn { host: escape(match[4]), pathname: escape(match[5]), }; -}; +} const formatMinidumpURL = ({scheme, host, pathname, publicKey}: Dsn) => { return `${scheme}${host}/api${pathname}/minidump/?sentry_key=${publicKey}`; @@ -99,8 +126,8 @@ const formatApiUrl = ({scheme, host}: Dsn) => { /** * Fetch project details from sentry */ -export async function fetchCodeKeywords() { - let json: {projects: ProjectApiResult[]} | null = null; +export async function fetchCodeKeywords(): Promise { + let json: {projects: ProjectApiResult[]; user: UserApiResult} | null = null; const url = process.env.NODE_ENV === 'development' @@ -125,7 +152,11 @@ export async function fetchCodeKeywords() { return makeDefaults(); } - const {projects} = json; + if (json === null) { + return makeDefaults(); + } + + const {projects, user} = json; if (projects?.length === 0) { return makeDefaults(); @@ -138,7 +169,7 @@ export async function fetchCodeKeywords() { DSN: project.dsn, PUBLIC_DSN: project.dsnPublic, PUBLIC_KEY: parsedDsn.publicKey, - SECRET_KEY: parsedDsn.secretKey, + SECRET_KEY: parsedDsn.secretKey ?? 'exampleSecretKey', API_URL: formatApiUrl(parsedDsn), PROJECT_ID: project.id, PROJECT_SLUG: project.projectSlug, @@ -150,24 +181,102 @@ export async function fetchCodeKeywords() { title: `${project.organizationSlug} / ${project.projectSlug}`, }; }), + USER: user.isAuthenticated + ? { + ID: user.id, + NAME: user.name, + } + : undefined, }; } -export function useCodeContextState(fetcher = fetchCodeKeywords) { +function getCsrfToken(): string | null { + // is sentry-sc in production, but may also be sc in other envs + // So we just try both variants + const cookieNames = ['sentry-sc', 'sc']; + + return cookieNames + .map(cookieName => Cookies.get(cookieName)) + .find(token => token !== null); +} + +export async function createOrgAuthToken({ + orgSlug, + name, +}: { + name: string; + orgSlug: string; +}) { + const baseUrl = + process.env.NODE_ENV === 'development' + ? 'http://dev.getsentry.net:8000/' + : 'https://sentry.io'; + + const url = `${baseUrl}/api/0/organizations/${orgSlug}/org-auth-tokens/`; + + const body = {name}; + + try { + const resp = await fetch(url, { + method: 'POST', + body: JSON.stringify(body), + credentials: 'include', + headers: { + Accept: 'application/json; charset=utf-8', + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken(), + }, + }); + + if (!resp.ok) { + return null; + } + + const json = await resp.json(); + + return json.token; + } catch { + return null; + } +} + +export function CodeContextProvider({children}: {children: React.ReactNode}) { const [codeKeywords, setCodeKeywords] = useState(cachedCodeKeywords ?? DEFAULTS); + const [isLoading, setIsLoading] = useState(cachedCodeKeywords ? false : true); + useEffect(() => { if (cachedCodeKeywords === null) { - fetcher().then((config: CodeKeywords) => { + setIsLoading(true); + fetchCodeKeywords().then((config: CodeKeywords) => { cachedCodeKeywords = config; setCodeKeywords(config); + setIsLoading(false); }); } - }); + }, [setIsLoading, setCodeKeywords]); - return { + // sharedKeywordSelection maintains a global mapping for each "keyword" + // namespace to the index of the selected item. + // + // NOTE: This ONLY does anything for the `PROJECT` keyword namespace, since + // that is the only namespace that actually has a list + const sharedKeywordSelection = useState>({}); + + // Maintains the global selection for which code block tab is selected + const sharedCodeSelection = useState(null); + + const result: CodeContextType = { codeKeywords, - sharedCodeSelection: useState(null), - sharedKeywordSelection: useState({}), + sharedCodeSelection, + sharedKeywordSelection, + isLoading, }; + + return {children}; +} + +/** For tests only. */ +export function _setCachedCodeKeywords(codeKeywords: CodeKeywords) { + cachedCodeKeywords = codeKeywords; } diff --git a/src/components/codeTabs.tsx b/src/components/codeTabs.tsx index 8fb240f661d94..219bb41d504d9 100644 --- a/src/components/codeTabs.tsx +++ b/src/components/codeTabs.tsx @@ -24,7 +24,7 @@ type Props = { children: JSX.Element | JSX.Element[]; }; -export function CodeTabs({children}: Props): JSX.Element { +export function CodeTabs({children}: Props) { if (!Array.isArray(children)) { children = [children]; } else { diff --git a/src/components/configKey.tsx b/src/components/configKey.tsx index d32ee36093137..82f5f5e51e555 100644 --- a/src/components/configKey.tsx +++ b/src/components/configKey.tsx @@ -17,7 +17,7 @@ export function ConfigKey({ notSupported = [], children, platform, -}: Props): JSX.Element { +}: Props) { // This is a literal copypaste of the HTML Gatsby outputs for regular // Markdown headings because we can't figure out how to make Gatsby // render component content like regular markdown/MDX content. We tried diff --git a/src/components/content.tsx b/src/components/content.tsx index e73c45da1877a..a0df59ef7b2b8 100644 --- a/src/components/content.tsx +++ b/src/components/content.tsx @@ -20,7 +20,7 @@ function RawHtml({html}) { return
; } -export function Content({file}: Props): JSX.Element | null { +export function Content({file}: Props) { if (!file) { return null; } diff --git a/src/components/definitionList.tsx b/src/components/definitionList.tsx index da67aadb7304c..d831b93e4ab91 100644 --- a/src/components/definitionList.tsx +++ b/src/components/definitionList.tsx @@ -1,5 +1,5 @@ import React from 'react'; -export function DefinitionList({children}: React.Props<{}>): JSX.Element { +export function DefinitionList({children}: {children: React.ReactNode}) { return
{children}
; } diff --git a/src/components/docsBotButton.tsx b/src/components/docsBotButton.tsx new file mode 100644 index 0000000000000..bd226c36ca6ab --- /dev/null +++ b/src/components/docsBotButton.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import styled from '@emotion/styled'; + +import SentryDocsBot from 'sentry-docs/logos/chatbot.svg'; + +export function DocsBotButton() { + return ( + + + Ask A Bot + + ); +} + +const DocsBotLink = styled('a')` + display: flex; + gap: 0.75rem; + height: 2.5rem; + background: #ffffff; + color: #231c3d; + border-radius: 0.5rem; + padding: 0.2rem 0.75rem; + line-height: 1.1; + align-items: center; + + transition-property: box-shadow, border-color; + transition-duration: 0.25s; + transition-timing-function: ease-out; + box-shadow: 0 2px 0 rgba(54, 45, 89, 0.15); + + &:hover { + box-shadow: 0 2px 0 rgba(54, 45, 89, 0.15), -0.1875rem -0.1875rem 0 0.1875rem #f2b712, + 0 0 0 0.375rem #e1567c; + text-decoration: none; + cursor: pointer; + } + + img { + flex-grow: 0; + flex-shrink: 1; + flex-basis: 50px; + } + + span { + white-space: nowrap; + } + + @media only screen and (max-width: 1200px) { + span { + display: none; + } + } +`; diff --git a/src/components/dynamicNav.tsx b/src/components/dynamicNav.tsx index 69da65cdea7b5..8476cdcbf5c1f 100644 --- a/src/components/dynamicNav.tsx +++ b/src/components/dynamicNav.tsx @@ -84,11 +84,7 @@ type ChildrenProps = { showDepth?: number; }; -export function Children({ - tree, - exclude = [], - showDepth = 0, -}: ChildrenProps): JSX.Element { +export function Children({tree, exclude = [], showDepth = 0}: ChildrenProps) { return {renderChildren(tree, exclude, showDepth)}; } @@ -114,7 +110,7 @@ export function DynamicNav({ prependLinks = [], suppressMissing = false, noHeadingLink = false, -}: Props): JSX.Element | null { +}: Props) { const location = useLocation(); if (root.startsWith('/')) { diff --git a/src/components/expandable.tsx b/src/components/expandable.tsx index 1ff5ba13f6b7f..d5568e0ec97d1 100644 --- a/src/components/expandable.tsx +++ b/src/components/expandable.tsx @@ -22,7 +22,7 @@ const ExpandableBody = styled.div` display: ${props => (props.isExpanded ? 'block' : 'none')}; `; -export function Expandable({title, children}: Props): JSX.Element { +export function Expandable({title, children}: Props) { const [isExpanded, setIsExpanded] = useState(false); return (
diff --git a/src/components/externalLink.tsx b/src/components/externalLink.tsx index a82e32eca0dd5..fffa44feac873 100644 --- a/src/components/externalLink.tsx +++ b/src/components/externalLink.tsx @@ -6,7 +6,7 @@ type Props = } | React.HTMLProps; -export function ExternalLink({children, ...props}: Props): JSX.Element { +export function ExternalLink({children, ...props}: Props) { return ( {children} diff --git a/src/components/githubCta.tsx b/src/components/githubCta.tsx index f6f4286f9a006..fd30a99cb3555 100644 --- a/src/components/githubCta.tsx +++ b/src/components/githubCta.tsx @@ -7,10 +7,7 @@ type GitHubCTAProps = { sourceInstanceName: string; }; -export function GitHubCTA({ - sourceInstanceName, - relativePath, -}: GitHubCTAProps): JSX.Element { +export function GitHubCTA({sourceInstanceName, relativePath}: GitHubCTAProps) { return (
Help improve this content diff --git a/src/components/guideGrid.tsx b/src/components/guideGrid.tsx index 09c5b56d13c6a..1d9d933f6bd33 100644 --- a/src/components/guideGrid.tsx +++ b/src/components/guideGrid.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {PlatformIcon} from 'platformicons'; -import {Platform, usePlatform} from './hooks/usePlatform'; +import {usePlatform} from './hooks/usePlatform'; import {SmartLink} from './smartLink'; type Props = { @@ -9,23 +9,22 @@ type Props = { platform?: string; }; -export function GuideGrid({platform, className}: Props): JSX.Element { +export function GuideGrid({platform, className}: Props) { const [currentPlatform] = usePlatform(platform); - // platform might actually not be a platform, so lets handle that case gracefully - if (!(currentPlatform as Platform).guides) { + + if (currentPlatform.type === 'guide') { return null; } return (