layout === 'table bottom'
- ? 'auto auto auto auto auto 1fr'
+ ? 'auto auto auto auto auto auto 1fr'
: layout === 'table right'
- ? 'min-content min-content min-content min-content min-content 1fr'
- : 'min-content min-content min-content min-content min-content 1fr'};
+ ? 'min-content min-content min-content min-content min-content min-content 1fr'
+ : 'min-content min-content min-content min-content min-content min-content 1fr'};
grid-template-columns: ${({layout}) =>
layout === 'table bottom'
? '100%'
@@ -315,8 +348,9 @@ const FlamegraphGrid = styled('div')<{
layout === 'table bottom'
? `
'minimap'
- 'ui-frames'
'spans'
+ 'ui-frames'
+ 'battery-chart'
'memory-chart'
'cpu-chart'
'flamegraph'
@@ -324,18 +358,20 @@ const FlamegraphGrid = styled('div')<{
`
: layout === 'table right'
? `
- 'minimap frame-stack'
- 'ui-frames frame-stack'
- 'spans frame-stack'
- 'memory-chart frame-stack'
- 'cpu-chart frame-stack'
- 'flamegraph frame-stack'
+ 'minimap frame-stack'
+ 'spans frame-stack'
+ 'ui-frames frame-stack'
+ 'battery-chart frame-stack'
+ 'memory-chart frame-stack'
+ 'cpu-chart frame-stack'
+ 'flamegraph frame-stack'
`
: layout === 'table left'
? `
'frame-stack minimap'
- 'frame-stack ui-frames'
'frame-stack spans'
+ 'frame-stack ui-frames'
+ 'frame-stack battery-chart'
'frame-stack memory-chart'
'frame-stack cpu-chart'
'frame-stack flamegraph'
@@ -385,6 +421,14 @@ const MemoryChartContainer = styled('div')<{
grid-area: memory-chart;
`;
+const BatteryChartContainer = styled('div')<{
+ containerHeight: FlamegraphTheme['SIZES']['BATTERY_CHART_HEIGHT'];
+}>`
+ position: relative;
+ height: ${p => p.containerHeight}px;
+ grid-area: battery-chart;
+`;
+
const UIFramesContainer = styled('div')<{
containerHeight: FlamegraphTheme['SIZES']['UI_FRAMES_HEIGHT'];
}>`
diff --git a/static/app/data/platformCategories.tsx b/static/app/data/platformCategories.tsx
index 56821296beadd2..7f7884d0f199e4 100644
--- a/static/app/data/platformCategories.tsx
+++ b/static/app/data/platformCategories.tsx
@@ -112,6 +112,12 @@ export const backend = [
'python-starlette',
'python-sanic',
'python-celery',
+ 'python-aiohttp',
+ 'python-chalice',
+ 'python-falcon',
+ 'python-quart',
+ 'python-tryton',
+ 'python-wsgi',
'python-bottle',
'python-pylons',
'python-pyramid',
@@ -129,6 +135,7 @@ export const serverless = [
'python-awslambda',
'python-azurefunctions',
'python-gcpfunctions',
+ 'python-serverless',
'node-awslambda',
'node-azurefunctions',
'node-gcpfunctions',
diff --git a/static/app/data/platforms.tsx b/static/app/data/platforms.tsx
index 7c1b358e87b82e..c4e654acbcc149 100644
--- a/static/app/data/platforms.tsx
+++ b/static/app/data/platforms.tsx
@@ -1,108 +1,689 @@
-import integrationDocsPlatforms from 'integration-docs-platforms';
import sortBy from 'lodash/sortBy';
import {t} from 'sentry/locale';
-import {PlatformIntegration} from 'sentry/types';
-
-import {tracing} from './platformCategories';
-
-const goPlatforms = [
- {
- integrations: [
- ...(integrationDocsPlatforms.platforms.find(platform => platform.id === 'go')
- ?.integrations ?? []),
- {
- link: 'https://docs.sentry.io/platforms/go/guides/echo/',
- type: 'framework',
- id: 'go-echo',
- name: t('Echo'),
- },
- {
- link: 'https://docs.sentry.io/platforms/go/guides/fasthttp/',
- type: 'framework',
- id: 'go-fasthttp',
- name: t('FastHTTP'),
- },
- {
- link: 'https://docs.sentry.io/platforms/go/guides/gin/',
- type: 'framework',
- id: 'go-gin',
- name: t('Gin'),
- },
- {
- link: 'https://docs.sentry.io/platforms/go/guides/http/',
- type: 'framework',
- id: 'go-http',
- name: t('Net/Http'),
- },
- {
- link: 'https://docs.sentry.io/platforms/go/guides/iris',
- type: 'framework',
- id: 'go-iris',
- name: t('Iris'),
- },
- {
- link: 'https://docs.sentry.io/platforms/go/guides/martini/',
- type: 'framework',
- id: 'go-martini',
- name: t('Martini'),
- },
- {
- link: 'https://docs.sentry.io/platforms/go/guides/negroni/',
- type: 'framework',
- id: 'go-negroni',
- name: t('Negroni'),
- },
- ],
+import type {PlatformIntegration} from 'sentry/types';
+
+type Platform = {
+ id: string;
+ language: string;
+ link: string;
+ name: string;
+ type: string;
+};
+
+const goPlatforms: Platform[] = [
+ {
id: 'go',
- name: t('Go'),
+ link: 'https://docs.sentry.io/platforms/go/',
+ name: 'Go',
+ type: 'language',
+ language: 'go',
+ },
+ {
+ link: 'https://docs.sentry.io/platforms/go/guides/echo/',
+ type: 'framework',
+ id: 'go-echo',
+ name: t('Echo'),
+ language: 'go',
+ },
+ {
+ link: 'https://docs.sentry.io/platforms/go/guides/fasthttp/',
+ type: 'framework',
+ id: 'go-fasthttp',
+ name: t('FastHTTP'),
+ language: 'go',
+ },
+ {
+ link: 'https://docs.sentry.io/platforms/go/guides/gin/',
+ type: 'framework',
+ id: 'go-gin',
+ name: t('Gin'),
+ language: 'go',
+ },
+ {
+ link: 'https://docs.sentry.io/platforms/go/guides/http/',
+ type: 'framework',
+ id: 'go-http',
+ name: t('Net/Http'),
+ language: 'go',
+ },
+ {
+ link: 'https://docs.sentry.io/platforms/go/guides/iris',
+ type: 'framework',
+ id: 'go-iris',
+ name: t('Iris'),
+ language: 'go',
+ },
+ {
+ link: 'https://docs.sentry.io/platforms/go/guides/martini/',
+ type: 'framework',
+ id: 'go-martini',
+ name: t('Martini'),
+ language: 'go',
+ },
+ {
+ link: 'https://docs.sentry.io/platforms/go/guides/negroni/',
+ type: 'framework',
+ id: 'go-negroni',
+ name: t('Negroni'),
+ language: 'go',
+ },
+];
+
+const javaScriptPlatforms: Platform[] = [
+ {
+ id: 'javascript-angular',
+ name: 'Angular',
+ type: 'framework',
+ language: 'javascript',
+ link: 'https://docs.sentry.io/platforms/javascript/guides/angular/',
+ },
+ {
+ id: 'javascript',
+ name: 'Browser JavaScript',
+ type: 'language',
+ language: 'javascript',
+ link: 'https://docs.sentry.io/platforms/javascript/',
+ },
+ {
+ id: 'javascript-ember',
+ name: 'Ember',
+ type: 'framework',
+ language: 'javascript',
+ link: 'https://docs.sentry.io/platforms/javascript/guides/ember/',
+ },
+ {
+ id: 'javascript-gatsby',
+ name: 'Gatsby',
+ type: 'framework',
+ language: 'javascript',
+ link: 'https://docs.sentry.io/platforms/javascript/guides/gatsby/',
+ },
+ {
+ id: 'javascript-nextjs',
+ name: 'Next.js',
+ type: 'framework',
+ language: 'javascript',
+ link: 'https://docs.sentry.io/platforms/javascript/guides/nextjs/',
+ },
+ {
+ id: 'javascript-react',
+ name: 'React',
+ type: 'framework',
+ language: 'javascript',
+ link: 'https://docs.sentry.io/platforms/javascript/guides/react/',
+ },
+ {
+ id: 'javascript-remix',
+ name: 'Remix',
+ type: 'framework',
+ language: 'javascript',
+ link: 'https://docs.sentry.io/platforms/javascript/guides/remix/',
+ },
+ {
+ id: 'javascript-svelte',
+ name: 'Svelte',
+ type: 'framework',
+ language: 'javascript',
+ link: 'https://docs.sentry.io/platforms/javascript/guides/svelte/',
+ },
+ {
+ id: 'javascript-sveltekit',
+ name: 'SvelteKit',
+ type: 'framework',
+ language: 'javascript',
+ link: 'https://docs.sentry.io/platforms/javascript/guides/sveltekit/',
+ },
+ {
+ id: 'javascript-vue',
+ name: 'Vue',
+ type: 'framework',
+ language: 'javascript',
+ link: 'https://docs.sentry.io/platforms/javascript/guides/vue/',
+ },
+];
+
+const javaPlatforms: Platform[] = [
+ {
+ id: 'java',
+ name: 'Java',
+ type: 'language',
+ language: 'java',
+ link: 'https://docs.sentry.io/platforms/java/',
+ },
+ {
+ id: 'java-log4j2',
+ name: 'Log4j 2.x',
+ type: 'framework',
+ language: 'java',
+ link: 'https://docs.sentry.io/platforms/java/guides/log4j2/',
+ },
+ {
+ id: 'java-logback',
+ name: 'Logback',
+ type: 'framework',
+ language: 'java',
+ link: 'https://docs.sentry.io/platforms/java/guides/logback/',
+ },
+ {
+ id: 'java-spring',
+ name: 'Spring',
+ type: 'framework',
+ language: 'java',
+ link: 'https://https://docs.sentry.io/platforms/java/guides/spring/',
+ },
+ {
+ id: 'java-spring-boot',
+ name: 'Spring Boot',
+ type: 'framework',
+ language: 'java',
+ link: 'https://docs.sentry.io/platforms/java/guides/spring-boot/',
},
];
-const platformIntegrations: PlatformIntegration[] = [
- ...integrationDocsPlatforms.platforms.filter(platform => platform.id !== 'go'),
+const pythonPlatforms: Platform[] = [
+ {
+ id: 'python-aiohttp',
+ name: 'AIOHTTP',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/aiohttp/',
+ },
+ {
+ id: 'python-asgi',
+ name: 'ASGI',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/asgi/',
+ },
+ {
+ id: 'python-awslambda',
+ name: 'AWS Lambda (Python)',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/aws-lambda/',
+ },
+ {
+ id: 'python-bottle',
+ name: 'Bottle',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/bottle/',
+ },
+ {
+ id: 'python-celery',
+ name: 'Celery',
+ type: 'library',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/celery/',
+ },
+ {
+ id: 'python-chalice',
+ name: 'Chalice',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/chalice/',
+ },
+ {
+ id: 'python-django',
+ name: 'Django',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/django/',
+ },
+ {
+ id: 'python-falcon',
+ name: 'Falcon',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/falcon/',
+ },
+ {
+ id: 'python-fastapi',
+ name: 'FastAPI',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/fastapi/',
+ },
+ {
+ id: 'python-flask',
+ name: 'Flask',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/flask/',
+ },
+ {
+ id: 'python-gcpfunctions',
+ name: 'Google Cloud Functions (Python)',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/gcp-functions/',
+ },
+ {
+ id: 'python-pymongo',
+ name: 'PyMongo',
+ type: 'library',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/pymongo/',
+ },
+ {
+ id: 'python-pylons',
+ name: 'Pylons',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/legacy-sdk/integrations/pylons/',
+ },
+ {
+ id: 'python-pyramid',
+ name: 'Pyramid',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/pyramid/',
+ },
+ {
+ id: 'python',
+ name: 'Python',
+ type: 'language',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/',
+ },
+ {
+ id: 'python-quart',
+ name: 'Quart',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/quart/',
+ },
+ {
+ id: 'python-rq',
+ name: 'RQ (Redis Queue)',
+ type: 'library',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/rq/',
+ },
+ {
+ id: 'python-sanic',
+ name: 'Sanic',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/sanic/',
+ },
+ {
+ id: 'python-serverless',
+ name: 'Serverless',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/serverless/',
+ },
+ {
+ id: 'python-starlette',
+ name: 'Starlette',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/starlette/',
+ },
+ {
+ id: 'python-tornado',
+ name: 'Tornado',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/tornado/',
+ },
+ {
+ id: 'python-tryton',
+ name: 'Tryton',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/tryton/',
+ },
+ {
+ id: 'python-wsgi',
+ name: 'WSGI',
+ type: 'framework',
+ language: 'python',
+ link: 'https://docs.sentry.io/platforms/python/guides/wsgi/',
+ },
+];
+
+const phpPlatforms: Platform[] = [
+ {
+ id: 'php-laravel',
+ name: 'Laravel',
+ type: 'framework',
+ language: 'php',
+ link: 'https://docs.sentry.io/platforms/php/guides/laravel/',
+ },
+ {
+ id: 'php',
+ name: 'PHP',
+ type: 'language',
+ language: 'php',
+ link: 'https://docs.sentry.io/platforms/php/',
+ },
+ {
+ id: 'php-symfony',
+ name: 'Symfony',
+ type: 'framework',
+ language: 'php',
+ link: 'https://docs.sentry.io/platforms/php/guides/symfony/',
+ },
+];
+
+const nodePlatforms: Platform[] = [
+ {
+ id: 'node-awslambda',
+ name: 'AWS Lambda (Node)',
+ type: 'framework',
+ language: 'node',
+ link: 'https://docs.sentry.io/platforms/node/guides/aws-lambda/',
+ },
+ {
+ id: 'node-azurefunctions',
+ name: 'Azure Functions (Node)',
+ type: 'framework',
+ language: 'node',
+ link: 'https://docs.sentry.io/platforms/node/guides/azure-functions/',
+ },
+ {
+ id: 'node-connect',
+ name: 'Connect',
+ type: 'framework',
+ language: 'node',
+ link: 'https://docs.sentry.io/platforms/node/guides/connect/',
+ },
+ {
+ id: 'node-express',
+ name: 'Express',
+ type: 'framework',
+ language: 'node',
+ link: 'https://docs.sentry.io/platforms/node/guides/express/',
+ },
+ {
+ id: 'node-gcpfunctions',
+ name: 'Google Cloud Functions (Node)',
+ type: 'framework',
+ language: 'node',
+ link: 'https://docs.sentry.io/platforms/node/guides/gcp-functions/',
+ },
+ {
+ id: 'node-koa',
+ name: 'Koa',
+ type: 'framework',
+ language: 'node',
+ link: 'https://docs.sentry.io/platforms/node/guides/koa/',
+ },
+ {
+ id: 'node',
+ name: 'Node.js',
+ type: 'language',
+ language: 'node',
+ link: 'https://docs.sentry.io/platforms/node/',
+ },
+ {
+ id: 'node-serverlesscloud',
+ name: 'Serverless (Node)',
+ type: 'framework',
+ language: 'node',
+ link: 'https://docs.sentry.io/platforms/node/guides/serverless-cloud/',
+ },
+];
+
+const dotNetPlatforms: Platform[] = [
+ {
+ id: 'dotnet',
+ name: '.NET',
+ type: 'language',
+ language: 'dotnet',
+ link: 'https://docs.sentry.io/platforms/dotnet/',
+ },
+ {
+ id: 'dotnet-aspnet',
+ name: 'ASP.NET',
+ type: 'framework',
+ language: 'dotnet',
+ link: 'https://docs.sentry.io/platforms/dotnet/guides/aspnet/',
+ },
+ {
+ id: 'dotnet-aspnetcore',
+ name: 'ASP.NET Core',
+ type: 'framework',
+ language: 'dotnet',
+ link: 'https://docs.sentry.io/platforms/dotnet/guides/aspnetcore/',
+ },
+ {
+ id: 'dotnet-awslambda',
+ name: 'AWS Lambda (.NET)',
+ type: 'framework',
+ language: 'dotnet',
+ link: 'https://docs.sentry.io/platforms/dotnet/guides/aws-lambda/',
+ },
+ {
+ id: 'dotnet-gcpfunctions',
+ name: 'Google Cloud Functions (.NET)',
+ type: 'framework',
+ language: 'dotnet',
+ link: 'https://docs.sentry.io/platforms/dotnet/guides/google-cloud-functions/',
+ },
+ {
+ id: 'dotnet-maui',
+ name: 'Multi-platform App UI (MAUI)',
+ type: 'framework',
+ language: 'dotnet',
+ link: 'https://docs.sentry.io/platforms/dotnet/guides/maui/',
+ },
+ {
+ id: 'dotnet-uwp',
+ name: 'UWP',
+ type: 'framework',
+ language: 'dotnet',
+ link: 'https://docs.sentry.io/platforms/dotnet/guides/uwp/',
+ },
+ {
+ id: 'dotnet-wpf',
+ name: 'WPF',
+ type: 'framework',
+ language: 'dotnet',
+ link: 'https://docs.sentry.io/platforms/dotnet/guides/wpf/',
+ },
+ {
+ id: 'dotnet-winforms',
+ name: 'Windows Forms',
+ type: 'framework',
+ language: 'dotnet',
+ link: 'https://docs.sentry.io/platforms/dotnet/guides/winforms/',
+ },
+ {
+ id: 'dotnet-xamarin',
+ name: 'Xamarin',
+ type: 'framework',
+ language: 'dotnet',
+ link: 'https://docs.sentry.io/platforms/dotnet/guides/xamarin/',
+ },
+];
+
+const applePlatforms: Platform[] = [
+ {
+ id: 'apple',
+ name: 'Apple',
+ type: 'language',
+ language: 'apple',
+ link: 'https://docs.sentry.io/platforms/apple/',
+ },
+ {
+ id: 'apple-ios',
+ name: 'iOS',
+ type: 'language',
+ language: 'apple',
+ link: 'https://docs.sentry.io/platforms/apple/',
+ },
+ {
+ id: 'apple-macos',
+ name: 'macOS',
+ type: 'language',
+ language: 'apple',
+ link: 'https://docs.sentry.io/platforms/apple/',
+ },
+];
+
+const rubyPlatforms: Platform[] = [
+ {
+ id: 'ruby-rack',
+ name: 'Rack Middleware',
+ type: 'framework',
+ language: 'ruby',
+ link: 'https://docs.sentry.io/platforms/ruby/guides/rack/',
+ },
+ {
+ id: 'ruby-rails',
+ name: 'Rails',
+ type: 'framework',
+ language: 'ruby',
+ link: 'https://docs.sentry.io/platforms/ruby/guides/rails/',
+ },
+ {
+ id: 'ruby',
+ name: 'Ruby',
+ type: 'language',
+ language: 'ruby',
+ link: 'https://docs.sentry.io/platforms/ruby/',
+ },
+];
+
+const nativePlatforms: Platform[] = [
+ {
+ id: 'native',
+ name: 'Native',
+ type: 'language',
+ language: 'native',
+ link: 'https://docs.sentry.io/platforms/native/',
+ },
+ {
+ id: 'native-qt',
+ name: 'Qt',
+ type: 'framework',
+ language: 'native',
+ link: 'https://docs.sentry.io/platforms/native/guides/qt/',
+ },
+];
+
+const otherPlatforms: Platform[] = [
+ {
+ id: 'android',
+ name: 'Android',
+ type: 'framework',
+ language: 'android',
+ link: 'https://docs.sentry.io/platforms/android/',
+ },
+ {
+ id: 'capacitor',
+ name: 'Capacitor',
+ type: 'framework',
+ language: 'capacitor',
+ link: 'https://docs.sentry.io/platforms/javascript/guides/capacitor/',
+ },
+ {
+ id: 'cordova',
+ name: 'Cordova',
+ type: 'language',
+ language: 'cordova',
+ link: 'https://docs.sentry.io/platforms/javascript/guides/cordova/',
+ },
+ {
+ id: 'dart',
+ name: 'Dart',
+ type: 'framework',
+ language: 'dart',
+ link: 'https://docs.sentry.io/platforms/dart/',
+ },
+ {
+ id: 'electron',
+ name: 'Electron',
+ type: 'language',
+ language: 'electron',
+ link: 'https://docs.sentry.io/platforms/javascript/guides/electron/',
+ },
+ {
+ id: 'elixir',
+ name: 'Elixir',
+ type: 'language',
+ language: 'elixir',
+ link: 'https://docs.sentry.io/platforms/elixir/',
+ },
+ {
+ id: 'flutter',
+ name: 'Flutter',
+ type: 'framework',
+ language: 'flutter',
+ link: 'https://docs.sentry.io/platforms/flutter/',
+ },
+ {
+ id: 'ionic',
+ name: 'Ionic',
+ type: 'framework',
+ language: 'ionic',
+ link: 'https://docs.sentry.io/platforms/javascript/guides/capacitor/',
+ },
+ {
+ id: 'kotlin',
+ name: 'Kotlin',
+ type: 'language',
+ language: 'kotlin',
+ link: 'https://docs.sentry.io/platforms/kotlin/',
+ },
+ {
+ id: 'minidump',
+ name: 'Minidump',
+ type: 'framework',
+ language: 'minidump',
+ link: 'https://docs.sentry.io/platforms/native/minidump/',
+ },
+ {
+ id: 'rust',
+ name: 'Rust',
+ type: 'language',
+ language: 'rust',
+ link: 'https://docs.sentry.io/platforms/rust/',
+ },
+ {
+ id: 'unity',
+ name: 'Unity',
+ type: 'framework',
+ language: 'unity',
+ link: 'https://docs.sentry.io/platforms/unity/',
+ },
+ {
+ id: 'unreal',
+ name: 'Unreal Engine',
+ type: 'framework',
+ language: 'unreal',
+ link: 'https://docs.sentry.io/platforms/unreal/',
+ },
+ {
+ id: 'react-native',
+ name: 'React Native',
+ type: 'language',
+ language: 'react-native',
+ link: 'https://docs.sentry.io/platforms/react-native/',
+ },
+];
+
+// If you update items of this list, please remember to update the "GETTING_STARTED_DOCS_PLATFORMS" list
+// in the 'src/sentry/models/project.py' file. This way, they'll work together correctly.
+// Otherwise, creating a project will cause an error in the backend, saying "Invalid platform".
+const allPlatforms = [
+ ...javaScriptPlatforms,
+ ...nodePlatforms,
+ ...dotNetPlatforms,
+ ...applePlatforms,
+ ...javaPlatforms,
+ ...pythonPlatforms,
+ ...phpPlatforms,
...goPlatforms,
-]
- .map(platform => {
- const integrations = platform.integrations.reduce((acc, value) => {
- // filter out any javascript-[angular|angularjs|ember|gatsby|nextjs|react|remix|svelte|sveltekit|vue]-* platforms; as they're not meant to be used as a platform in the PlatformPicker component
- if (value.id.match('^javascript-([A-Za-z]+)-([a-zA-Z0-9]+.*)$')) {
- return acc;
- }
-
- // filter out any tracing platforms; as they're not meant to be used as a platform for
- // the project creation flow
- if ((tracing as readonly string[]).includes(value.id)) {
- return acc;
- }
-
- // filter out any performance onboarding documentation
- if (value.id.includes('performance-onboarding')) {
- return acc;
- }
-
- // filter out any replay onboarding documentation
- if (value.id.includes('replay-onboarding')) {
- return acc;
- }
-
- // filter out any profiling onboarding documentation
- if (value.id.includes('profiling-onboarding')) {
- return acc;
- }
-
- if (!acc[value.id]) {
- acc[value.id] = {...value, language: platform.id};
- return acc;
- }
-
- return acc;
- }, {});
-
- return Object.values(integrations) as PlatformIntegration[];
- })
- .flat();
-
-const platforms = sortBy(platformIntegrations, 'id');
+ ...rubyPlatforms,
+ ...nativePlatforms,
+ ...otherPlatforms,
+];
+
+const platforms = sortBy(allPlatforms, 'id') as PlatformIntegration[];
export default platforms;
diff --git a/static/app/gettingStartedDocs/python/aiohttp.spec.tsx b/static/app/gettingStartedDocs/python/aiohttp.spec.tsx
index c1b9ddf6a4d0ec..a92dea7e96b6e5 100644
--- a/static/app/gettingStartedDocs/python/aiohttp.spec.tsx
+++ b/static/app/gettingStartedDocs/python/aiohttp.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithAIOHTTP', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/aiohttp.tsx b/static/app/gettingStartedDocs/python/aiohttp.tsx
index acea5ca5c2cf7f..b45cc5b1c44f16 100644
--- a/static/app/gettingStartedDocs/python/aiohttp.tsx
+++ b/static/app/gettingStartedDocs/python/aiohttp.tsx
@@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
const introduction = (
{tct(
@@ -17,8 +29,10 @@ const introduction = (
);
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: (
@@ -60,15 +74,7 @@ import sentry_sdk
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- AioHttpIntegration(),
- ],
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
from aiohttp import web
@@ -87,8 +93,37 @@ web.run_app(app)
];
// Configuration End
-export function GettingStartedWithAIOHTTP({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithAIOHTTP({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[AioHttpIntegration()],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithAIOHTTP;
diff --git a/static/app/gettingStartedDocs/python/awslambda.spec.tsx b/static/app/gettingStartedDocs/python/awslambda.spec.tsx
index 3406c669bf5898..19acf6ddd1afee 100644
--- a/static/app/gettingStartedDocs/python/awslambda.spec.tsx
+++ b/static/app/gettingStartedDocs/python/awslambda.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithAwsLambda', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content', dsn: 'test-dsn'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/awslambda.tsx b/static/app/gettingStartedDocs/python/awslambda.tsx
index f2563efc656390..14bc04cfe49634 100644
--- a/static/app/gettingStartedDocs/python/awslambda.tsx
+++ b/static/app/gettingStartedDocs/python/awslambda.tsx
@@ -6,10 +6,22 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
const introduction = (
{tct(
@@ -25,7 +37,11 @@ const introduction = (
export const steps = ({
dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ dsn: string;
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: (
@@ -51,15 +67,7 @@ import sentry_sdk
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- AwsLambdaIntegration(),
- ],
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
def my_function(event, context):
@@ -108,10 +116,6 @@ sentry_sdk.init(
integrations=[
AwsLambdaIntegration(timeout_warning=True),
],
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
)
`,
},
@@ -123,10 +127,35 @@ sentry_sdk.init(
];
// Configuration End
-export function GettingStartedWithAwsLambda({dsn, ...props}: ModuleProps) {
+export function GettingStartedWithAwsLambda({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[AwsLambdaIntegration()],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
return (
-
+
{tct(
'If you are using another web framework inside of AWS Lambda, the framework might catch those exceptions before we get to see them. Make sure to enable the framework specific integration as well, if one exists. See [link:Integrations] for more information.',
diff --git a/static/app/gettingStartedDocs/python/bottle.spec.tsx b/static/app/gettingStartedDocs/python/bottle.spec.tsx
index f9260a6a640182..052731acba579b 100644
--- a/static/app/gettingStartedDocs/python/bottle.spec.tsx
+++ b/static/app/gettingStartedDocs/python/bottle.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithBottle', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/bottle.tsx b/static/app/gettingStartedDocs/python/bottle.tsx
index bab134199e265e..04ab294cb9f1da 100644
--- a/static/app/gettingStartedDocs/python/bottle.tsx
+++ b/static/app/gettingStartedDocs/python/bottle.tsx
@@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
const introduction = (
{tct(
@@ -17,8 +29,10 @@ const introduction = (
);
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: (
@@ -54,14 +68,7 @@ from bottle import Bottle, run
from sentry_sdk.integrations.bottle import BottleIntegration
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- BottleIntegration(),
- ],
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
app = Bottle()
@@ -72,8 +79,37 @@ app = Bottle()
];
// Configuration End
-export function GettingStartedWithBottle({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithBottle({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[BottleIntegration()],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithBottle;
diff --git a/static/app/gettingStartedDocs/python/celery.spec.tsx b/static/app/gettingStartedDocs/python/celery.spec.tsx
index 6f63b36600bfda..3b7622b6d77091 100644
--- a/static/app/gettingStartedDocs/python/celery.spec.tsx
+++ b/static/app/gettingStartedDocs/python/celery.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithCelery', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/celery.tsx b/static/app/gettingStartedDocs/python/celery.tsx
index 53ebe57dea3e0a..87edc336e6dbc5 100644
--- a/static/app/gettingStartedDocs/python/celery.tsx
+++ b/static/app/gettingStartedDocs/python/celery.tsx
@@ -4,9 +4,21 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
const introduction = (
{tct('The celery integration adds support for the [link:Celery Task Queue System].', {
@@ -16,8 +28,10 @@ const introduction = (
);
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.CONFIGURE,
description: (
@@ -39,15 +53,7 @@ import sentry_sdk
from sentry_sdk.integrations.celery import CeleryIntegration
sentry_sdk.init(
- dsn='${dsn}',
- integrations=[
- CeleryIntegration(),
- ],
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
`,
},
@@ -66,8 +72,37 @@ sentry_sdk.init(
];
// Configuration End
-export function GettingStartedWithCelery({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithCelery({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[CeleryIntegration()],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithCelery;
diff --git a/static/app/gettingStartedDocs/python/chalice.spec.tsx b/static/app/gettingStartedDocs/python/chalice.spec.tsx
index 954771244c5cd5..d6228184d05d8e 100644
--- a/static/app/gettingStartedDocs/python/chalice.spec.tsx
+++ b/static/app/gettingStartedDocs/python/chalice.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithChalice', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/chalice.tsx b/static/app/gettingStartedDocs/python/chalice.tsx
index 24c1c97a6ec997..d28d32db0d36ca 100644
--- a/static/app/gettingStartedDocs/python/chalice.tsx
+++ b/static/app/gettingStartedDocs/python/chalice.tsx
@@ -1,13 +1,26 @@
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {tct} from 'sentry/locale';
// Configuration Start
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: (
@@ -37,15 +50,7 @@ from sentry_sdk.integrations.chalice import ChaliceIntegration
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- ChaliceIntegration(),
- ],
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
app = Chalice(app_name="appname")
@@ -56,8 +61,35 @@ app = Chalice(app_name="appname")
];
// Configuration End
-export function GettingStartedWithChalice({dsn, ...props}: ModuleProps) {
- return ;
-}
+export function GettingStartedWithChalice({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[ChaliceIntegration()],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
+}
export default GettingStartedWithChalice;
diff --git a/static/app/gettingStartedDocs/python/django.tsx b/static/app/gettingStartedDocs/python/django.tsx
index 5062654d390ba3..9fab29d4c37d60 100644
--- a/static/app/gettingStartedDocs/python/django.tsx
+++ b/static/app/gettingStartedDocs/python/django.tsx
@@ -7,19 +7,19 @@ import {t, tct} from 'sentry/locale';
// Configuration Start
-const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
- # of sampled transactions.
- # We recommend adjusting this value in production.
- profiles_sample_rate=1.0,`;
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
-const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production.
- traces_sample_rate=1.0,`;
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
-const piiConfiguration = ` # If you wish to associate users to errors (assuming you are using
- # django.contrib.auth) you may enable sending PII data.
- send_default_pii=True,`;
+const piiConfiguration = ` # If you wish to associate users to errors (assuming you are using
+ # django.contrib.auth) you may enable sending PII data.
+ send_default_pii=True,`;
export const steps = ({
sentryInitContent,
@@ -80,12 +80,12 @@ ${sentryInitContent}
from django.urls import path
def trigger_error(request):
- division_by_zero = 1 / 0
+ division_by_zero = 1 / 0
- urlpatterns = [
- path('sentry-debug/', trigger_error),
- # ...
- ]
+ urlpatterns = [
+ path('sentry-debug/', trigger_error),
+ # ...
+ ]
`,
},
],
@@ -104,8 +104,8 @@ export function GettingStartedWithDjango({
const otherConfigs: string[] = [];
let sentryInitContent: string[] = [
- ` dsn="${dsn}",`,
- ` integrations=[DjangoIntegration()],`,
+ ` dsn="${dsn}",`,
+ ` integrations=[DjangoIntegration()],`,
piiConfiguration,
];
diff --git a/static/app/gettingStartedDocs/python/falcon.spec.tsx b/static/app/gettingStartedDocs/python/falcon.spec.tsx
index 5f27afdc562610..1c888ed1c2a673 100644
--- a/static/app/gettingStartedDocs/python/falcon.spec.tsx
+++ b/static/app/gettingStartedDocs/python/falcon.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithFalcon', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/falcon.tsx b/static/app/gettingStartedDocs/python/falcon.tsx
index e3fa713c34eebd..6c0617c34a94d4 100644
--- a/static/app/gettingStartedDocs/python/falcon.tsx
+++ b/static/app/gettingStartedDocs/python/falcon.tsx
@@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
const introduction = (
{tct(
@@ -17,8 +29,10 @@ const introduction = (
);
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: (
@@ -53,15 +67,7 @@ import sentry_sdk
from sentry_sdk.integrations.falcon import FalconIntegration
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- FalconIntegration(),
- ],
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
api = falcon.API()
@@ -72,8 +78,37 @@ api = falcon.API()
];
// Configuration End
-export function GettingStartedWithFalcon({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithFalcon({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[FalconIntegration()],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithFalcon;
diff --git a/static/app/gettingStartedDocs/python/fastapi.spec.tsx b/static/app/gettingStartedDocs/python/fastapi.spec.tsx
index e836ef82b995d5..8eb49e13340da5 100644
--- a/static/app/gettingStartedDocs/python/fastapi.spec.tsx
+++ b/static/app/gettingStartedDocs/python/fastapi.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithFastApi', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/fastapi.tsx b/static/app/gettingStartedDocs/python/fastapi.tsx
index c8a8c3c7f0af0f..b6fdefe1e2ced0 100644
--- a/static/app/gettingStartedDocs/python/fastapi.tsx
+++ b/static/app/gettingStartedDocs/python/fastapi.tsx
@@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
const introduction = (
{tct('The FastAPI integration adds support for the [link:FastAPI Framework].', {
@@ -14,8 +26,10 @@ const introduction = (
);
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: (
@@ -51,12 +65,7 @@ import sentry_sdk
sentry_sdk.init(
- dsn="${dsn}",
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
app = FastAPI()
@@ -100,8 +109,34 @@ division_by_zero = 1 / 0
];
// Configuration End
-export function GettingStartedWithFastApi({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithFastApi({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [` dsn="${dsn}",`];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithFastApi;
diff --git a/static/app/gettingStartedDocs/python/flask.spec.tsx b/static/app/gettingStartedDocs/python/flask.spec.tsx
index d3495777731c22..39ab492bc57642 100644
--- a/static/app/gettingStartedDocs/python/flask.spec.tsx
+++ b/static/app/gettingStartedDocs/python/flask.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithDjango', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/flask.tsx b/static/app/gettingStartedDocs/python/flask.tsx
index 83ad6ba926c975..d29e46d5e94d43 100644
--- a/static/app/gettingStartedDocs/python/flask.tsx
+++ b/static/app/gettingStartedDocs/python/flask.tsx
@@ -2,12 +2,26 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: (
@@ -54,15 +68,7 @@ from flask import Flask
from sentry_sdk.integrations.flask import FlaskIntegration
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- FlaskIntegration(),
- ],
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production.
- traces_sample_rate=1.0
+${sentryInitContent}
)
app = Flask(__name__)
@@ -100,8 +106,36 @@ def trigger_error():
];
// Configuration End
-export function GettingStartedWithFlask({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithFlask({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[FlaskIntegration()],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithFlask;
diff --git a/static/app/gettingStartedDocs/python/gcpfunctions.spec.tsx b/static/app/gettingStartedDocs/python/gcpfunctions.spec.tsx
index febad135a18a42..f24fddcfdaa53e 100644
--- a/static/app/gettingStartedDocs/python/gcpfunctions.spec.tsx
+++ b/static/app/gettingStartedDocs/python/gcpfunctions.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithGCPFunctions', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content', dsn: 'test-dsn'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/gcpfunctions.tsx b/static/app/gettingStartedDocs/python/gcpfunctions.tsx
index 31a165a0698c12..2c2b3189ffe614 100644
--- a/static/app/gettingStartedDocs/python/gcpfunctions.tsx
+++ b/static/app/gettingStartedDocs/python/gcpfunctions.tsx
@@ -6,13 +6,29 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
export const steps = ({
dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ dsn: string;
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: (
@@ -38,19 +54,11 @@ import sentry_sdk
from sentry_sdk.integrations.gcp import GcpIntegration
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- GcpIntegration(),
- ],
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
def http_function_entrypoint(request):
- ...
+ ...
`,
},
],
@@ -91,15 +99,10 @@ def http_function_entrypoint(request):
language: 'python',
code: `
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- GcpIntegration(timeout_warning=True),
- ],
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+ dsn="${dsn}",
+ integrations=[
+ GcpIntegration(timeout_warning=True),
+ ],
)
`,
},
@@ -111,10 +114,34 @@ sentry_sdk.init(
];
// Configuration End
-export function GettingStartedWithGCPFunctions({dsn, ...props}: ModuleProps) {
+export function GettingStartedWithGCPFunctions({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[GcpIntegration()],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
return (
-
+
{tct(
'If you are using a web framework in your Cloud Function, the framework might catch those exceptions before we get to see them. Make sure to enable the framework specific integration as well, if one exists. See [link:Integrations] for more information.',
diff --git a/static/app/gettingStartedDocs/python/pyramid.spec.tsx b/static/app/gettingStartedDocs/python/pyramid.spec.tsx
index ff05c418454580..d2019c494a2cb0 100644
--- a/static/app/gettingStartedDocs/python/pyramid.spec.tsx
+++ b/static/app/gettingStartedDocs/python/pyramid.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithPyramid', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/pyramid.tsx b/static/app/gettingStartedDocs/python/pyramid.tsx
index 7fb8994f8d8491..224ab2b221ef05 100644
--- a/static/app/gettingStartedDocs/python/pyramid.tsx
+++ b/static/app/gettingStartedDocs/python/pyramid.tsx
@@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
const introduction = (
{tct(
@@ -17,8 +29,10 @@ const introduction = (
);
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: {tct('Install [code:sentry-sdk] from PyPI:', {code:
})}
,
@@ -45,26 +59,18 @@ import sentry_sdk
from sentry_sdk.integrations.pyramid import PyramidIntegration
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- PyramidIntegration(),
- ],
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production.
- traces_sample_rate=1.0,
+${sentryInitContent}
)
def sentry_debug(request):
-division_by_zero = 1 / 0
+ division_by_zero = 1 / 0
with Configurator() as config:
-config.add_route('sentry-debug', '/')
-config.add_view(sentry_debug, route_name='sentry-debug')
-app = config.make_wsgi_app()
-server = make_server('0.0.0.0', 6543, app)
-server.serve_forever()
+ config.add_route('sentry-debug', '/')
+ config.add_view(sentry_debug, route_name='sentry-debug')
+ app = config.make_wsgi_app()
+ server = make_server('0.0.0.0', 6543, app)
+ server.serve_forever()
`,
},
],
@@ -72,8 +78,37 @@ server.serve_forever()
];
// Configuration End
-export function GettingStartedWithPyramid({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithPyramid({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[PyramidIntegration()],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithPyramid;
diff --git a/static/app/gettingStartedDocs/python/python.tsx b/static/app/gettingStartedDocs/python/python.tsx
index dbfd0fbf90b035..4b9c23b9d9d7c9 100644
--- a/static/app/gettingStartedDocs/python/python.tsx
+++ b/static/app/gettingStartedDocs/python/python.tsx
@@ -6,15 +6,15 @@ import {t, tct} from 'sentry/locale';
// Configuration Start
-const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
- # of sampled transactions.
- # We recommend adjusting this value in production.
- profiles_sample_rate=1.0,`;
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
-const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production.
- traces_sample_rate=1.0,`;
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
export const steps = ({
sentryInitContent,
@@ -87,7 +87,7 @@ export function GettingStartedWithPython({
}: ModuleProps) {
const otherConfigs: string[] = [];
- let sentryInitContent: string[] = [` dsn="${dsn}",`];
+ let sentryInitContent: string[] = [` dsn="${dsn}",`];
if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
otherConfigs.push(performanceConfiguration);
diff --git a/static/app/gettingStartedDocs/python/quart.spec.tsx b/static/app/gettingStartedDocs/python/quart.spec.tsx
index a795acab8c67f3..773506f59d18c5 100644
--- a/static/app/gettingStartedDocs/python/quart.spec.tsx
+++ b/static/app/gettingStartedDocs/python/quart.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithDjango', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/quart.tsx b/static/app/gettingStartedDocs/python/quart.tsx
index 9295db403c08da..ff63823952b5d0 100644
--- a/static/app/gettingStartedDocs/python/quart.tsx
+++ b/static/app/gettingStartedDocs/python/quart.tsx
@@ -4,9 +4,21 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
const introduction = (
@@ -22,8 +34,10 @@ const introduction = (
);
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: {tct('Install [code:sentry-sdk] from PyPI:', {code:
})}
,
@@ -48,14 +62,7 @@ from sentry_sdk.integrations.quart import QuartIntegration
from quart import Quart
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- QuartIntegration(),
- ],
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
app = Quart(__name__)
@@ -66,8 +73,37 @@ app = Quart(__name__)
];
// Configuration End
-export function GettingStartedWithQuart({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithQuart({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[QuartIntegration()],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithQuart;
diff --git a/static/app/gettingStartedDocs/python/rq.spec.tsx b/static/app/gettingStartedDocs/python/rq.spec.tsx
index 29dacd3d8acc45..4f713c4ef0517d 100644
--- a/static/app/gettingStartedDocs/python/rq.spec.tsx
+++ b/static/app/gettingStartedDocs/python/rq.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithRq', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/rq.tsx b/static/app/gettingStartedDocs/python/rq.tsx
index 53beac47fbf502..3917e1f684fb27 100644
--- a/static/app/gettingStartedDocs/python/rq.tsx
+++ b/static/app/gettingStartedDocs/python/rq.tsx
@@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
const introduction = (
{tct('The RQ integration adds support for the [link:RQ Job Queue System].', {
@@ -14,8 +26,10 @@ const introduction = (
);
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.CONFIGURE,
description: (
@@ -33,15 +47,7 @@ import sentry_sdk
from sentry_sdk.integrations.rq import RqIntegration
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- RqIntegration(),
- ],
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
`,
},
@@ -59,8 +65,37 @@ rq worker \
];
// Configuration End
-export function GettingStartedWithRq({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithRq({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[RqIntegration()],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithRq;
diff --git a/static/app/gettingStartedDocs/python/sanic.spec.tsx b/static/app/gettingStartedDocs/python/sanic.spec.tsx
index 26f65fd9ae9925..a3dfb9efa2af7d 100644
--- a/static/app/gettingStartedDocs/python/sanic.spec.tsx
+++ b/static/app/gettingStartedDocs/python/sanic.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithSanic', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/sanic.tsx b/static/app/gettingStartedDocs/python/sanic.tsx
index 632b287b39442a..0caab573f61115 100644
--- a/static/app/gettingStartedDocs/python/sanic.tsx
+++ b/static/app/gettingStartedDocs/python/sanic.tsx
@@ -6,9 +6,21 @@ import ListItem from 'sentry/components/list/listItem';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
const introduction = (
@@ -40,8 +52,10 @@ const introduction = (
);
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: {tct('Install [code:sentry-sdk] from PyPI:', {code:
})}
,
@@ -80,15 +94,7 @@ from sentry_sdk.integrations.sanic import SanicIntegration
from sanic import Sanic
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- SanicIntegration(),
- ],
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
app = Sanic(__name__)
@@ -99,8 +105,37 @@ app = Sanic(__name__)
];
// Configuration End
-export function GettingStartedWithSanic({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithSanic({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[SanicIntegration()],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithSanic;
diff --git a/static/app/gettingStartedDocs/python/serverless.spec.tsx b/static/app/gettingStartedDocs/python/serverless.spec.tsx
index ce863883be3dfe..aacca3d8c2109e 100644
--- a/static/app/gettingStartedDocs/python/serverless.spec.tsx
+++ b/static/app/gettingStartedDocs/python/serverless.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithServerless', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/serverless.tsx b/static/app/gettingStartedDocs/python/serverless.tsx
index e8ee834185a065..93a9088b547c01 100644
--- a/static/app/gettingStartedDocs/python/serverless.tsx
+++ b/static/app/gettingStartedDocs/python/serverless.tsx
@@ -4,17 +4,26 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
-// It is recommended to use an integration for your particular serverless environment if available, as those are easier to use and capture more useful information.
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
-// If you use a serverless provider not directly supported by the SDK, you can use this generic integration.
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: (
@@ -50,12 +59,7 @@ import sentry_sdk
from sentry_sdk.integrations.serverless import serverless_function
sentry_sdk.init(
- dsn="${dsn}",
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
@serverless_function
@@ -67,8 +71,33 @@ def my_function(...): ...
];
// Configuration End
-export function GettingStartedWithServerless({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithServerless({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [` dsn="${dsn}",`];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithServerless;
diff --git a/static/app/gettingStartedDocs/python/starlette.spec.tsx b/static/app/gettingStartedDocs/python/starlette.spec.tsx
index cb07fe3381e80f..8586e8d2a56e8a 100644
--- a/static/app/gettingStartedDocs/python/starlette.spec.tsx
+++ b/static/app/gettingStartedDocs/python/starlette.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithDjango', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/starlette.tsx b/static/app/gettingStartedDocs/python/starlette.tsx
index fbcfc06f7cee78..495e8d530a595d 100644
--- a/static/app/gettingStartedDocs/python/starlette.tsx
+++ b/static/app/gettingStartedDocs/python/starlette.tsx
@@ -2,18 +2,33 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
const introduction = tct(
'The Starlette integration adds support for the Starlette Framework.',
{
link: ,
}
);
+
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: (
@@ -54,12 +69,7 @@ import sentry_sdk
sentry_sdk.init(
- dsn="${dsn}",
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
app = Starlette(routes=[...])
@@ -91,10 +101,10 @@ from starlette.routing import Route
async def trigger_error(request):
- division_by_zero = 1 / 0
+ division_by_zero = 1 / 0
app = Starlette(routes=[
- Route("/sentry-debug", trigger_error),
+ Route("/sentry-debug", trigger_error),
])
`,
additionalInfo: t(
@@ -106,8 +116,34 @@ app = Starlette(routes=[
];
// Configuration End
-export function GettingStartedWithStarlette({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithStarlette({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [` dsn="${dsn}",`];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithStarlette;
diff --git a/static/app/gettingStartedDocs/python/tornado.spec.tsx b/static/app/gettingStartedDocs/python/tornado.spec.tsx
index 55c07396966c19..b0cf9ccd005179 100644
--- a/static/app/gettingStartedDocs/python/tornado.spec.tsx
+++ b/static/app/gettingStartedDocs/python/tornado.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithTornado', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/tornado.tsx b/static/app/gettingStartedDocs/python/tornado.tsx
index 3c8fa7779bce9d..2e056a3d36e4a7 100644
--- a/static/app/gettingStartedDocs/python/tornado.tsx
+++ b/static/app/gettingStartedDocs/python/tornado.tsx
@@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
const introduction = (
{tct(
@@ -17,8 +29,10 @@ const introduction = (
);
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: {tct('Install [code:sentry-sdk] from PyPI:', {code:
})}
,
@@ -54,15 +68,7 @@ import sentry_sdk
from sentry_sdk.integrations.tornado import TornadoIntegration
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- TornadoIntegration(),
- ],
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
# Your app code here, without changes
@@ -76,8 +82,37 @@ class MyHandler(...):
];
// Configuration End
-export function GettingStartedWithTornado({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithTornado({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[TornadoIntegration()],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithTornado;
diff --git a/static/app/gettingStartedDocs/python/tryton.spec.tsx b/static/app/gettingStartedDocs/python/tryton.spec.tsx
index a8274467ffcc9d..6c376929b18a95 100644
--- a/static/app/gettingStartedDocs/python/tryton.spec.tsx
+++ b/static/app/gettingStartedDocs/python/tryton.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithTryton', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/tryton.tsx b/static/app/gettingStartedDocs/python/tryton.tsx
index b090a14bdb4b86..ddcaef21fc7cc8 100644
--- a/static/app/gettingStartedDocs/python/tryton.tsx
+++ b/static/app/gettingStartedDocs/python/tryton.tsx
@@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
const introduction = (
{tct('The Tryton integration adds support for the [link:Tryton Framework Server].', {
@@ -14,8 +26,10 @@ const introduction = (
);
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.CONFIGURE,
description: (
@@ -37,15 +51,7 @@ import sentry_sdk
import sentry_sdk.integrations.trytond
sentry_sdk.init(
- dsn="${dsn}",
- integrations=[
- sentry_sdk.integrations.trytond.TrytondWSGIIntegration(),
- ],
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
from trytond.application import app as application
@@ -67,12 +73,12 @@ from trytond.exceptions import UserError
@application.error_handler
def _(app, request, e):
- if isinstance(e, TrytonException):
- return
- else:
- event_id = sentry_sdk.last_event_id()
- data = UserError('Custom message', f'{event_id}{e}')
- return app.make_response(request, data)
+ if isinstance(e, TrytonException):
+ return
+ else:
+ event_id = sentry_sdk.last_event_id()
+ data = UserError('Custom message', f'{event_id}{e}')
+ return app.make_response(request, data)
`,
},
],
@@ -80,8 +86,39 @@ def _(app, request, e):
];
// Configuration End
-export function GettingStartedWithTryton({dsn, ...props}: ModuleProps) {
- return ;
+export function GettingStartedWithTryton({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [
+ ` dsn="${dsn}",`,
+ ` integrations=[`,
+ ` sentry_sdk.integrations.trytond.TrytondWSGIIntegration(),`,
+ ` ],`,
+ ];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
}
export default GettingStartedWithTryton;
diff --git a/static/app/gettingStartedDocs/python/wsgi.spec.tsx b/static/app/gettingStartedDocs/python/wsgi.spec.tsx
index c21523a9f12477..e2a8a3f2e09ada 100644
--- a/static/app/gettingStartedDocs/python/wsgi.spec.tsx
+++ b/static/app/gettingStartedDocs/python/wsgi.spec.tsx
@@ -9,7 +9,7 @@ describe('GettingStartedWithWSGI', function () {
const {container} = render();
// Steps
- for (const step of steps()) {
+ for (const step of steps({sentryInitContent: 'test-init-content'})) {
expect(
screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]})
).toBeInTheDocument();
diff --git a/static/app/gettingStartedDocs/python/wsgi.tsx b/static/app/gettingStartedDocs/python/wsgi.tsx
index 7fe3b4a7f000c4..825413290180ba 100644
--- a/static/app/gettingStartedDocs/python/wsgi.tsx
+++ b/static/app/gettingStartedDocs/python/wsgi.tsx
@@ -4,12 +4,26 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {t, tct} from 'sentry/locale';
// Configuration Start
+
+const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100%
+ # of sampled transactions.
+ # We recommend adjusting this value in production.
+ profiles_sample_rate=1.0,`;
+
+const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100%
+ # of transactions for performance monitoring.
+ # We recommend adjusting this value in production.
+ traces_sample_rate=1.0,`;
+
export const steps = ({
- dsn,
-}: Partial> = {}): LayoutProps['steps'] => [
+ sentryInitContent,
+}: {
+ sentryInitContent: string;
+}): LayoutProps['steps'] => [
{
type: StepType.INSTALL,
description: (
@@ -39,12 +53,7 @@ from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from myapp import wsgi_app
sentry_sdk.init(
- dsn="${dsn}",
-
- # Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
- # We recommend adjusting this value in production,
- traces_sample_rate=1.0,
+${sentryInitContent}
)
wsgi_app = SentryWsgiMiddleware(wsgi_app)
@@ -55,8 +64,32 @@ wsgi_app = SentryWsgiMiddleware(wsgi_app)
];
// Configuration End
-export function GettingStartedWithWSGI({dsn, ...props}: ModuleProps) {
- return ;
-}
+export function GettingStartedWithWSGI({
+ dsn,
+ activeProductSelection = [],
+ ...props
+}: ModuleProps) {
+ const otherConfigs: string[] = [];
+
+ let sentryInitContent: string[] = [` dsn="${dsn}",`];
+
+ if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) {
+ otherConfigs.push(performanceConfiguration);
+ }
+ if (activeProductSelection.includes(ProductSolution.PROFILING)) {
+ otherConfigs.push(profilingConfiguration);
+ }
+
+ sentryInitContent = sentryInitContent.concat(otherConfigs);
+
+ return (
+
+ );
+}
export default GettingStartedWithWSGI;
diff --git a/static/app/types/group.tsx b/static/app/types/group.tsx
index 46285ee81f393d..6ee6a316d5e81f 100644
--- a/static/app/types/group.tsx
+++ b/static/app/types/group.tsx
@@ -72,7 +72,7 @@ export enum IssueType {
PERFORMANCE_UNCOMPRESSED_ASSET = 'performance_uncompressed_assets',
PERFORMANCE_LARGE_HTTP_PAYLOAD = 'performance_large_http_payload',
PERFORMANCE_HTTP_OVERHEAD = 'performance_http_overhead',
- PERFORMANCE_P95_TRANSACTION_DURATION_REGRESSION = 'performance_p95_transaction_duration_regression',
+ PERFORMANCE_DURATION_REGRESSION = 'performance_duration_regression',
// Profile
PROFILE_FILE_IO_MAIN_THREAD = 'profile_file_io_main_thread',
diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx
index be34c2885da5e1..e3063ace248d29 100644
--- a/static/app/utils/discover/fieldRenderers.tsx
+++ b/static/app/utils/discover/fieldRenderers.tsx
@@ -50,7 +50,7 @@ import {
} from 'sentry/views/performance/transactionSummary/filter';
import {PercentChangeCell} from 'sentry/views/starfish/components/tableCells/percentChangeCell';
import {TimeSpentCell} from 'sentry/views/starfish/components/tableCells/timeSpentCell';
-import {SpanMetricsFields} from 'sentry/views/starfish/types';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
import {decodeScalar} from '../queryString';
@@ -769,7 +769,7 @@ const SPECIAL_FUNCTIONS: SpecialFunctions = {
return (
);
},
diff --git a/static/app/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext.tsx b/static/app/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext.tsx
index 129b61c744e059..274f47954d7de9 100644
--- a/static/app/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext.tsx
+++ b/static/app/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext.tsx
@@ -19,6 +19,7 @@ export const DEFAULT_FLAMEGRAPH_STATE: FlamegraphState = {
},
preferences: {
timelines: {
+ battery_chart: true,
ui_frames: true,
minimap: true,
transaction_spans: true,
diff --git a/static/app/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphPreferences.tsx b/static/app/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphPreferences.tsx
index b94da8e65aa8a6..533df808e6a763 100644
--- a/static/app/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphPreferences.tsx
+++ b/static/app/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphPreferences.tsx
@@ -18,6 +18,7 @@ export interface FlamegraphPreferences {
layout: 'table right' | 'table bottom' | 'table left';
sorting: FlamegraphSorting;
timelines: {
+ battery_chart: boolean;
cpu_chart: boolean;
memory_chart: boolean;
minimap: boolean;
diff --git a/static/app/utils/profiling/flamegraph/flamegraphTheme.tsx b/static/app/utils/profiling/flamegraph/flamegraphTheme.tsx
index 09a71bd91455c4..b8c920250d8acc 100644
--- a/static/app/utils/profiling/flamegraph/flamegraphTheme.tsx
+++ b/static/app/utils/profiling/flamegraph/flamegraphTheme.tsx
@@ -46,6 +46,7 @@ export interface FlamegraphTheme {
// They should instead be defined as arrays of numbers so we can use them with glsl and avoid unnecessary parsing
COLORS: {
BAR_LABEL_FONT_COLOR: string;
+ BATTERY_CHART_COLORS: ColorChannels[];
CHART_CURSOR_INDICATOR: string;
CHART_LABEL_COLOR: string;
COLOR_BUCKET: (t: number) => ColorChannels;
@@ -96,6 +97,7 @@ export interface FlamegraphTheme {
BAR_FONT_SIZE: number;
BAR_HEIGHT: number;
BAR_PADDING: number;
+ BATTERY_CHART_HEIGHT: number;
CHART_PX_PADDING: number;
CPU_CHART_HEIGHT: number;
FLAMEGRAPH_DEPTH_OFFSET: number;
@@ -154,6 +156,7 @@ const SIZES: FlamegraphTheme['SIZES'] = {
BAR_FONT_SIZE: 11,
BAR_HEIGHT: 20,
BAR_PADDING: 4,
+ BATTERY_CHART_HEIGHT: 80,
FLAMEGRAPH_DEPTH_OFFSET: 12,
HOVERED_FRAME_BORDER_WIDTH: 2,
HIGHLIGHTED_FRAME_BORDER_WIDTH: 3,
@@ -186,6 +189,7 @@ export const LightFlamegraphTheme: FlamegraphTheme = {
SIZES,
COLORS: {
BAR_LABEL_FONT_COLOR: '#000',
+ BATTERY_CHART_COLORS: [[0.4, 0.56, 0.9, 0.65]],
COLOR_BUCKET: makeColorBucketTheme(LCH_LIGHT),
SPAN_COLOR_BUCKET: makeColorBucketTheme(SPAN_LCH_LIGHT, 140, 220),
COLOR_MAPS: {
@@ -239,6 +243,7 @@ export const DarkFlamegraphTheme: FlamegraphTheme = {
SIZES,
COLORS: {
BAR_LABEL_FONT_COLOR: 'rgb(255 255 255 / 80%)',
+ BATTERY_CHART_COLORS: [[0.4, 0.56, 0.9, 0.5]],
COLOR_BUCKET: makeColorBucketTheme(LCH_DARK),
SPAN_COLOR_BUCKET: makeColorBucketTheme(SPANS_LCH_DARK, 140, 220),
COLOR_MAPS: {
@@ -252,8 +257,8 @@ export const DarkFlamegraphTheme: FlamegraphTheme = {
},
CPU_CHART_COLORS: CHART_PALETTE[12].map(c => hexToColorChannels(c, 0.8)),
MEMORY_CHART_COLORS: [
- hexToColorChannels(CHART_PALETTE[4][2], 0.8),
- hexToColorChannels(CHART_PALETTE[4][3], 0.8),
+ hexToColorChannels(CHART_PALETTE[4][2], 0.5),
+ hexToColorChannels(CHART_PALETTE[4][3], 0.5),
],
CHART_CURSOR_INDICATOR: 'rgba(255, 255, 255, 0.5)',
CHART_LABEL_COLOR: 'rgba(255, 255, 255, 0.5)',
@@ -262,8 +267,8 @@ export const DarkFlamegraphTheme: FlamegraphTheme = {
DIFFERENTIAL_INCREASE: [0.98, 0.2058, 0.4381],
FOCUSED_FRAME_BORDER_COLOR: darkTheme.focus,
FRAME_GRAYSCALE_COLOR: [0.5, 0.5, 0.5, 0.4],
- FRAME_APPLICATION_COLOR: [0.1, 0.1, 0.8, 0.4],
- FRAME_SYSTEM_COLOR: [0.7, 0.1, 0.1, 0.5],
+ FRAME_APPLICATION_COLOR: [0.1, 0.1, 0.5, 0.4],
+ FRAME_SYSTEM_COLOR: [0.6, 0.15, 0.25, 0.3],
SPAN_FALLBACK_COLOR: [1, 1, 1, 0.3],
GRID_FRAME_BACKGROUND_COLOR: 'rgb(26, 20, 31,1)',
GRID_LINE_COLOR: '#222227',
diff --git a/static/app/utils/replays/replayReader.spec.tsx b/static/app/utils/replays/replayReader.spec.tsx
index ac243a125e832e..bbcde5bf6e547e 100644
--- a/static/app/utils/replays/replayReader.spec.tsx
+++ b/static/app/utils/replays/replayReader.spec.tsx
@@ -91,6 +91,14 @@ describe('ReplayReader', () => {
startTimestamp: new Date('2023-12-25T00:03:00'),
endTimestamp: new Date('2023-12-25T00:03:30'),
});
+ const navCrumb = TestStubs.Replay.BreadcrumbFrameEvent({
+ timestamp: new Date('2023-12-25T00:03:00'),
+ data: {
+ payload: TestStubs.Replay.NavFrame({
+ timestamp: new Date('2023-12-25T00:03:00'),
+ }),
+ },
+ });
const consoleEvent = TestStubs.Replay.ConsoleEvent({timestamp});
const customEvent = TestStubs.Replay.BreadcrumbFrameEvent({
timestamp: new Date('2023-12-25T00:02:30'),
@@ -114,6 +122,7 @@ describe('ReplayReader', () => {
firstDiv,
firstMemory,
navigationEvent,
+ navCrumb,
optionsEvent,
secondDiv,
secondMemory,
@@ -170,6 +179,7 @@ describe('ReplayReader', () => {
expected: [
expect.objectContaining({category: 'replay.init'}),
expect.objectContaining({category: 'ui.slowClickDetected'}),
+ expect.objectContaining({category: 'navigation'}),
expect.objectContaining({op: 'navigation.navigate'}),
expect.objectContaining({category: 'ui.click'}),
expect.objectContaining({category: 'ui.click'}),
diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx
index 3aa0a1faa9faf4..7f9768324ee176 100644
--- a/static/app/utils/replays/replayReader.tsx
+++ b/static/app/utils/replays/replayReader.tsx
@@ -29,6 +29,8 @@ import {
BreadcrumbCategories,
isDeadClick,
isDeadRageClick,
+ isLCPFrame,
+ isPaintFrame,
} from 'sentry/utils/replays/types';
import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
@@ -245,27 +247,34 @@ export default class ReplayReader {
);
getChapterFrames = memoize(() =>
+ [
+ ...this.getPerfFrames(),
+ ...this._sortedBreadcrumbFrames.filter(frame =>
+ ['replay.init', 'replay.mutations'].includes(frame.category)
+ ),
+ ...this._errors,
+ ].sort(sortFrames)
+ );
+
+ getPerfFrames = memoize(() =>
[
...removeDuplicateClicks(
this._sortedBreadcrumbFrames.filter(
frame =>
- ['navigation', 'replay.init', 'replay.mutations', 'ui.click'].includes(
- frame.category
- ) ||
+ ['navigation', 'ui.click'].includes(frame.category) ||
(frame.category === 'ui.slowClickDetected' &&
(isDeadClick(frame as SlowClickFrame) ||
isDeadRageClick(frame as SlowClickFrame)))
)
),
- ...this._sortedSpanFrames.filter(frame =>
- ['navigation.navigate', 'navigation.reload', 'navigation.back_forward'].includes(
- frame.op
- )
- ),
- ...this._errors,
+ ...this._sortedSpanFrames.filter(frame => frame.op.startsWith('navigation.')),
].sort(sortFrames)
);
+ getLPCFrames = memoize(() => this._sortedSpanFrames.filter(isLCPFrame));
+
+ getPaintFrames = memoize(() => this._sortedSpanFrames.filter(isPaintFrame));
+
getSDKOptions = () => this._optionFrame;
isNetworkDetailsSetup = memoize(() => {
diff --git a/static/app/utils/replays/types.tsx b/static/app/utils/replays/types.tsx
index eeaa2451647dba..9fd38b6d1e0084 100644
--- a/static/app/utils/replays/types.tsx
+++ b/static/app/utils/replays/types.tsx
@@ -89,6 +89,14 @@ export function isConsoleFrame(frame: BreadcrumbFrame): frame is ConsoleFrame {
return false;
}
+export function isLCPFrame(frame: SpanFrame): frame is LargestContentfulPaintFrame {
+ return frame.op === 'largest-contentful-paint';
+}
+
+export function isPaintFrame(frame: SpanFrame): frame is PaintFrame {
+ return frame.op === 'paint';
+}
+
export function isDeadClick(frame: SlowClickFrame) {
return frame.data.endReason === 'timeout';
}
diff --git a/static/app/views/dashboards/exportDashboard.tsx b/static/app/views/dashboards/exportDashboard.tsx
index 59b9dea6880f90..9ba1064f5b44cd 100644
--- a/static/app/views/dashboards/exportDashboard.tsx
+++ b/static/app/views/dashboards/exportDashboard.tsx
@@ -17,10 +17,11 @@ async function exportDashboard() {
const structure = {
base_url: null,
dashboard_id: null,
+ org_slug: null,
};
const params = getAPIParams(structure);
- const apiUrl = `https://${params.base_url}/api/0/organizations/testorg-az/dashboards/${params.dashboard_id}/`;
+ const apiUrl = `https://${params.base_url}/api/0/organizations/${params.org_slug}/dashboards/${params.dashboard_id}/`;
const response = await fetch(apiUrl);
const jsonData = await response.json();
const normalized = normalizeData(jsonData);
@@ -39,6 +40,7 @@ function getAPIParams(structure) {
const regex = {
base_url: /(\/\/)(.*?)(\/)/,
dashboard_id: /(dashboard\/)(.*?)(\/)/,
+ org_slug: /(\/\/)(.+?)(?=\.)/,
};
for (const attr in regex) {
@@ -122,6 +124,7 @@ function getPropertyStructure(property) {
queries: [],
displayType: '',
widgetType: '',
+ layout: [],
};
break;
case 'queries':
diff --git a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx
index 8557d6e6e8412c..73fc9df82de29c 100644
--- a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx
+++ b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx
@@ -14,6 +14,7 @@ import {EventEvidence} from 'sentry/components/events/eventEvidence';
import {EventExtraData} from 'sentry/components/events/eventExtraData';
import EventReplay from 'sentry/components/events/eventReplay';
import {EventSdk} from 'sentry/components/events/eventSdk';
+import EventBreakpointChart from 'sentry/components/events/eventStatisticalDetector/breakpointChart';
import RegressionMessage from 'sentry/components/events/eventStatisticalDetector/regressionMessage';
import {EventTagsAndScreenshot} from 'sentry/components/events/eventTagsAndScreenshot';
import {EventViewHierarchy} from 'sentry/components/events/eventViewHierarchy';
@@ -85,11 +86,17 @@ function GroupEventDetailsContent({
const eventEntryProps = {group, event, project};
- if (group.issueType === IssueType.PERFORMANCE_P95_TRANSACTION_DURATION_REGRESSION) {
+ if (group.issueType === IssueType.PERFORMANCE_DURATION_REGRESSION) {
return (
- // TODO: Swap this feature flag with the statistical detector flag
-
-
+
+
+
+
+
);
}
diff --git a/static/app/views/onboarding/setupDocs.spec.tsx b/static/app/views/onboarding/setupDocs.spec.tsx
index d7c4d5cddb1659..066b2a4f87889c 100644
--- a/static/app/views/onboarding/setupDocs.spec.tsx
+++ b/static/app/views/onboarding/setupDocs.spec.tsx
@@ -411,7 +411,7 @@ describe('Onboarding Setup Docs', function () {
);
expect(
- await screen.findByRole('heading', {name: 'Configure JavaScript SDK'})
+ await screen.findByRole('heading', {name: 'Configure Browser JavaScript SDK'})
).toBeInTheDocument();
expect(updateLoaderMock).toHaveBeenCalledTimes(1);
diff --git a/static/app/views/performance/database/databaseLandingPage.tsx b/static/app/views/performance/database/databaseLandingPage.tsx
index 1971bd415afb39..6267d602b0432c 100644
--- a/static/app/views/performance/database/databaseLandingPage.tsx
+++ b/static/app/views/performance/database/databaseLandingPage.tsx
@@ -11,7 +11,7 @@ import {space} from 'sentry/styles/space';
import useOrganization from 'sentry/utils/useOrganization';
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
import {ModulePageProviders} from 'sentry/views/performance/database/modulePageProviders';
-import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types';
+import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types';
import {ActionSelector} from 'sentry/views/starfish/views/spans/selectors/actionSelector';
import {DomainSelector} from 'sentry/views/starfish/views/spans/selectors/domainSelector';
import SpansTable from 'sentry/views/starfish/views/spans/spansTable';
@@ -64,12 +64,12 @@ function DatabaseLandingPage() {
diff --git a/static/app/views/performance/database/databaseSpanSummaryPage.tsx b/static/app/views/performance/database/databaseSpanSummaryPage.tsx
index efe8f76d8e51bd..32c8fa3379e980 100644
--- a/static/app/views/performance/database/databaseSpanSummaryPage.tsx
+++ b/static/app/views/performance/database/databaseSpanSummaryPage.tsx
@@ -26,7 +26,7 @@ import {
useSpanMetrics,
} from 'sentry/views/starfish/queries/useSpanMetrics';
import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useSpanMetricsSeries';
-import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types';
+import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types';
import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
import {
getDurationChartTitle,
@@ -43,7 +43,7 @@ type Query = {
endpointMethod: string;
transaction: string;
transactionMethod: string;
- [QueryParameterNames.SORT]: string;
+ [QueryParameterNames.SPANS_SORT]: string;
};
type Props = {
@@ -69,43 +69,43 @@ function SpanSummaryPage({params}: Props) {
groupId,
queryFilter,
[
- SpanMetricsFields.SPAN_OP,
- SpanMetricsFields.SPAN_DESCRIPTION,
- SpanMetricsFields.SPAN_ACTION,
- SpanMetricsFields.SPAN_DOMAIN,
+ SpanMetricsField.SPAN_OP,
+ SpanMetricsField.SPAN_DESCRIPTION,
+ SpanMetricsField.SPAN_ACTION,
+ SpanMetricsField.SPAN_DOMAIN,
'count()',
- `${StarfishFunctions.SPM}()`,
- `sum(${SpanMetricsFields.SPAN_SELF_TIME})`,
- `avg(${SpanMetricsFields.SPAN_SELF_TIME})`,
- `${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`,
- `${StarfishFunctions.HTTP_ERROR_COUNT}()`,
+ `${SpanFunction.SPM}()`,
+ `sum(${SpanMetricsField.SPAN_SELF_TIME})`,
+ `avg(${SpanMetricsField.SPAN_SELF_TIME})`,
+ `${SpanFunction.TIME_SPENT_PERCENTAGE}()`,
+ `${SpanFunction.HTTP_ERROR_COUNT}()`,
],
'api.starfish.span-summary-page-metrics'
);
const span = {
...spanMetrics,
- [SpanMetricsFields.SPAN_GROUP]: groupId,
+ [SpanMetricsField.SPAN_GROUP]: groupId,
} as {
- [SpanMetricsFields.SPAN_OP]: string;
- [SpanMetricsFields.SPAN_DESCRIPTION]: string;
- [SpanMetricsFields.SPAN_ACTION]: string;
- [SpanMetricsFields.SPAN_DOMAIN]: string;
- [SpanMetricsFields.SPAN_GROUP]: string;
+ [SpanMetricsField.SPAN_OP]: string;
+ [SpanMetricsField.SPAN_DESCRIPTION]: string;
+ [SpanMetricsField.SPAN_ACTION]: string;
+ [SpanMetricsField.SPAN_DOMAIN]: string;
+ [SpanMetricsField.SPAN_GROUP]: string;
};
const {isLoading: areSpanMetricsSeriesLoading, data: spanMetricsSeriesData} =
useSpanMetricsSeries(
groupId,
queryFilter,
- [`avg(${SpanMetricsFields.SPAN_SELF_TIME})`, 'spm()', 'http_error_count()'],
+ [`avg(${SpanMetricsField.SPAN_SELF_TIME})`, 'spm()', 'http_error_count()'],
'api.starfish.span-summary-page-metrics-chart'
);
useSynchronizeCharts([!areSpanMetricsSeriesLoading]);
const spanMetricsThroughputSeries = {
- seriesName: span?.[SpanMetricsFields.SPAN_OP]?.startsWith('db')
+ seriesName: span?.[SpanMetricsField.SPAN_OP]?.startsWith('db')
? 'Queries'
: 'Requests',
data: spanMetricsSeriesData?.['spm()'].data,
@@ -155,14 +155,14 @@ function SpanSummaryPage({params}: Props) {
- {span?.[SpanMetricsFields.SPAN_DESCRIPTION] && (
+ {span?.[SpanMetricsField.SPAN_DESCRIPTION] && (
@@ -171,7 +171,7 @@ function SpanSummaryPage({params}: Props) {
-
+
diff --git a/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx b/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx
index c96860c9aa9810..06271e5b41a59f 100644
--- a/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx
+++ b/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx
@@ -292,4 +292,4 @@ export function TrendsWidget(props: PerformanceWidgetProps) {
);
}
-const TrendsChart = withProjects(Chart);
+export const TrendsChart = withProjects(Chart);
diff --git a/static/app/views/projectInstall/platform.spec.tsx b/static/app/views/projectInstall/platform.spec.tsx
index 8899268495a3ef..00734c29520b6a 100644
--- a/static/app/views/projectInstall/platform.spec.tsx
+++ b/static/app/views/projectInstall/platform.spec.tsx
@@ -126,7 +126,7 @@ describe('ProjectInstallPlatform', function () {
expect(
await screen.findByRole('heading', {
- name: 'Configure JavaScript SDK',
+ name: 'Configure Browser JavaScript SDK',
})
).toBeInTheDocument();
});
diff --git a/static/app/views/replays/detail/console/consoleLogRow.tsx b/static/app/views/replays/detail/console/consoleLogRow.tsx
index 7cdf040914d17f..6614c5e44345d0 100644
--- a/static/app/views/replays/detail/console/consoleLogRow.tsx
+++ b/static/app/views/replays/detail/console/consoleLogRow.tsx
@@ -11,7 +11,7 @@ import type useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
import type {BreadcrumbFrame, ConsoleFrame} from 'sentry/utils/replays/types';
import MessageFormatter from 'sentry/views/replays/detail/console/messageFormatter';
import TimestampButton from 'sentry/views/replays/detail/timestampButton';
-import type {OnDimensionChange} from 'sentry/views/replays/detail/useVirtualListDimentionChange';
+import type {OnDimensionChange} from 'sentry/views/replays/detail/useVirtualizedInspector';
interface Props extends ReturnType {
currentHoverTime: number | undefined;
diff --git a/static/app/views/replays/detail/layout/index.tsx b/static/app/views/replays/detail/layout/index.tsx
index 0c145775eba484..e548c19233b851 100644
--- a/static/app/views/replays/detail/layout/index.tsx
+++ b/static/app/views/replays/detail/layout/index.tsx
@@ -77,7 +77,7 @@ function ReplayLayout({layout = LayoutKey.TOPBAR}: Props) {
);
- const hasSize = width + height;
+ const hasSize = width + height > 0;
if (layout === LayoutKey.NO_VIDEO) {
return (
diff --git a/static/app/views/replays/detail/perfTable/grabber.tsx b/static/app/views/replays/detail/perfTable/grabber.tsx
new file mode 100644
index 00000000000000..3fea352f67f7d6
--- /dev/null
+++ b/static/app/views/replays/detail/perfTable/grabber.tsx
@@ -0,0 +1,51 @@
+import type {MouseEventHandler} from 'react';
+import styled from '@emotion/styled';
+
+interface Props {
+ 'data-is-held': boolean;
+ 'data-slide-direction': 'leftright' | 'updown';
+ onDoubleClick: MouseEventHandler;
+ onMouseDown: MouseEventHandler;
+}
+
+const Grabber = styled('div')`
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 6px;
+ transform: translate(-3px, 0);
+ z-index: ${p => p.theme.zIndex.initial};
+
+ user-select: inherit;
+ &[data-is-held='true'] {
+ user-select: none;
+ }
+
+ &[data-slide-direction='leftright'] {
+ cursor: ew-resize;
+ }
+ &[data-slide-direction='updown'] {
+ cursor: ns-resize;
+ }
+
+ &:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 2.5px;
+ height: 100%;
+ width: 1px;
+ transform: translate(-0.5px, 0);
+ z-index: ${p => p.theme.zIndex.initial};
+ background: ${p => p.theme.border};
+ }
+ &:hover:after,
+ &[data-is-held='true']:after {
+ left: 1.5px;
+ width: 3px;
+ background: ${p => p.theme.black};
+ }
+`;
+
+export default Grabber;
diff --git a/static/app/views/replays/detail/perfTable/index.tsx b/static/app/views/replays/detail/perfTable/index.tsx
index 8c0d6ee832766a..14d2a9104ddda0 100644
--- a/static/app/views/replays/detail/perfTable/index.tsx
+++ b/static/app/views/replays/detail/perfTable/index.tsx
@@ -1,15 +1,14 @@
-// import {useReplayContext} from 'sentry/components/replays/replayContext';
-// import PerfTable from 'sentry/views/replays/detail/perfTable/perfTable';
-// import useReplayPerfData from 'sentry/views/replays/detail/perfTable/useReplayPerfData';
+import {useReplayContext} from 'sentry/components/replays/replayContext';
+import PerfTable from 'sentry/views/replays/detail/perfTable/perfTable';
+import useReplayPerfData from 'sentry/views/replays/detail/perfTable/useReplayPerfData';
type Props = {};
function Perf({}: Props) {
- // const {replay} = useReplayContext();
- // const perfData = useReplayPerfData({replay});
+ const {replay} = useReplayContext();
+ const perfData = useReplayPerfData({replay});
- // return ;
- return ;
+ return ;
}
export default Perf;
diff --git a/static/app/views/replays/detail/perfTable/perfFilters.tsx b/static/app/views/replays/detail/perfTable/perfFilters.tsx
new file mode 100644
index 00000000000000..642f7ab20b56f5
--- /dev/null
+++ b/static/app/views/replays/detail/perfTable/perfFilters.tsx
@@ -0,0 +1,34 @@
+import type {SelectOption} from 'sentry/components/compactSelect';
+import {CompactSelect} from 'sentry/components/compactSelect';
+import {t} from 'sentry/locale';
+import FiltersGrid from 'sentry/views/replays/detail/filtersGrid';
+import usePerfFilters from 'sentry/views/replays/detail/perfTable/usePerfFilters';
+
+type Props = {
+ traceRows: undefined | unknown[];
+} & ReturnType;
+
+function PerfFilters({getCrumbTypes, selectValue, setFilters}: Props) {
+ const crumbTypes = getCrumbTypes();
+ return (
+
+ []) => void}
+ options={[
+ {
+ label: t('Type'),
+ options: crumbTypes,
+ },
+ ]}
+ size="sm"
+ triggerLabel={selectValue?.length === 0 ? t('Any') : null}
+ triggerProps={{prefix: t('Filter')}}
+ value={selectValue}
+ />
+
+ );
+}
+
+export default PerfFilters;
diff --git a/static/app/views/replays/detail/perfTable/perfRow.tsx b/static/app/views/replays/detail/perfTable/perfRow.tsx
new file mode 100644
index 00000000000000..a27ae46436d84e
--- /dev/null
+++ b/static/app/views/replays/detail/perfTable/perfRow.tsx
@@ -0,0 +1,162 @@
+import {CSSProperties, Fragment, useCallback} from 'react';
+import styled from '@emotion/styled';
+import classNames from 'classnames';
+
+import BreadcrumbIcon from 'sentry/components/events/interfaces/breadcrumbs/breadcrumb/type/icon';
+import {Tooltip} from 'sentry/components/tooltip';
+import {IconClock, IconRefresh} from 'sentry/icons';
+import {tct} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import getFrameDetails from 'sentry/utils/replays/getFrameDetails';
+import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
+import IconWrapper from 'sentry/views/replays/detail/iconWrapper';
+import TraceGrid from 'sentry/views/replays/detail/perfTable/traceGrid';
+import type {ReplayTraceRow} from 'sentry/views/replays/detail/perfTable/useReplayPerfData';
+import TimestampButton from 'sentry/views/replays/detail/timestampButton';
+import {OnDimensionChange} from 'sentry/views/replays/detail/useVirtualListDimentionChange';
+
+interface Props {
+ currentHoverTime: number | undefined;
+ currentTime: number;
+ index: number;
+ onDimensionChange: OnDimensionChange;
+ startTimestampMs: number;
+ style: CSSProperties;
+ traceRow: ReplayTraceRow;
+}
+
+export default function PerfRow({
+ currentHoverTime,
+ currentTime,
+ index,
+ onDimensionChange,
+ startTimestampMs,
+ style,
+ traceRow,
+}: Props) {
+ const {lcpFrames, replayFrame: frame, paintFrames, flattenedTraces} = traceRow;
+ const {color, description, title, type} = getFrameDetails(frame);
+ const lcp = lcpFrames.length ? getFrameDetails(lcpFrames[0]) : null;
+
+ const handleDimensionChange = useCallback(
+ () => onDimensionChange(index),
+ [onDimensionChange, index]
+ );
+
+ const {onMouseEnter, onMouseLeave, onClickTimestamp} = useCrumbHandlers();
+
+ const handleClickTimestamp = useCallback(
+ () => onClickTimestamp(frame),
+ [onClickTimestamp, frame]
+ );
+ const handleMouseEnter = useCallback(() => onMouseEnter(frame), [onMouseEnter, frame]);
+ const handleMouseLeave = useCallback(() => onMouseLeave(frame), [onMouseLeave, frame]);
+
+ const hasOccurred = frame ? currentTime >= frame.offsetMs : false;
+ const isBeforeHover = frame
+ ? currentHoverTime === undefined || currentHoverTime >= frame.offsetMs
+ : false;
+
+ return (
+
+
+
+
+
+
+
+ {title}
+
+ {description}
+
+
+
+ {lcp ? (
+
+
+ {tct('[lcp] LCP', {lcp: lcp.description})}
+
+ ) : null}
+
+
+ {paintFrames.length ? (
+
+
+ {tct('[count] paint events', {count: paintFrames.length})}
+
+ ) : null}
+
+
+
+ {flattenedTraces.map((flatTrace, i) => (
+
+ ))}
+
+
+ );
+}
+
+const PerfListItem = styled('div')`
+ padding: ${space(1)} ${space(1.5)};
+
+ /* Overridden in TabItemContainer, depending on *CurrentTime and *HoverTime classes */
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+`;
+
+const Vertical = styled('div')`
+ display: flex;
+ flex-direction: column;
+ overflow-x: hidden;
+`;
+
+const Horizontal = styled('div')`
+ display: flex;
+ flex: auto 1 1;
+ flex-direction: row;
+
+ font-size: ${p => p.theme.fontSizeSmall};
+ overflow: auto;
+`;
+
+const Title = styled('span')<{hasOccurred?: boolean}>`
+ color: ${p => (p.hasOccurred ? p.theme.gray400 : p.theme.gray300)};
+ font-size: ${p => p.theme.fontSizeMedium};
+ font-weight: bold;
+ line-height: ${p => p.theme.text.lineHeightBody};
+ text-transform: capitalize;
+ ${p => p.theme.overflowEllipsis};
+`;
+
+const Description = styled(Tooltip)`
+ ${p => p.theme.overflowEllipsis};
+ font-size: 0.7rem;
+ font-variant-numeric: tabular-nums;
+ line-height: ${p => p.theme.text.lineHeightBody};
+ color: ${p => p.theme.subText};
+`;
+
+const IconLabel = styled('span')`
+ display: flex;
+ align-items: center;
+ align-self: baseline;
+ gap: 4px;
+`;
diff --git a/static/app/views/replays/detail/perfTable/perfTable.tsx b/static/app/views/replays/detail/perfTable/perfTable.tsx
new file mode 100644
index 00000000000000..59cb7ce19817d7
--- /dev/null
+++ b/static/app/views/replays/detail/perfTable/perfTable.tsx
@@ -0,0 +1,111 @@
+import {useMemo, useRef} from 'react';
+import {
+ AutoSizer,
+ CellMeasurer,
+ List as ReactVirtualizedList,
+ ListRowProps,
+} from 'react-virtualized';
+
+import Placeholder from 'sentry/components/placeholder';
+import {useReplayContext} from 'sentry/components/replays/replayContext';
+import {t} from 'sentry/locale';
+import FilterLoadingIndicator from 'sentry/views/replays/detail/filterLoadingIndicator';
+import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
+import NoRowRenderer from 'sentry/views/replays/detail/noRowRenderer';
+import PerfFilters from 'sentry/views/replays/detail/perfTable/perfFilters';
+import PerfRow from 'sentry/views/replays/detail/perfTable/perfRow';
+import usePerfFilters from 'sentry/views/replays/detail/perfTable/usePerfFilters';
+import type useReplayPerfData from 'sentry/views/replays/detail/perfTable/useReplayPerfData';
+import TabItemContainer from 'sentry/views/replays/detail/tabItemContainer';
+import useVirtualizedList from 'sentry/views/replays/detail/useVirtualizedList';
+import useVirtualListDimentionChange from 'sentry/views/replays/detail/useVirtualListDimentionChange';
+
+interface Props {
+ perfData: ReturnType;
+}
+
+const cellMeasurer = {
+ fixedWidth: true,
+ minHeight: 24,
+};
+
+export default function PerfTable({perfData}: Props) {
+ const {currentTime, currentHoverTime, replay} = useReplayContext();
+ const startTimestampMs = replay?.getReplay().started_at.getTime() ?? 0;
+
+ const traceRows = perfData.data;
+
+ const filterProps = usePerfFilters({traceRows: traceRows || []});
+ const {items} = filterProps; // setSearchTerm
+ const clearSearchTerm = () => {}; // setSearchTerm('');
+
+ const listRef = useRef(null);
+ const deps = useMemo(() => [items], [items]);
+ const {cache, updateList} = useVirtualizedList({
+ cellMeasurer,
+ ref: listRef,
+ deps,
+ });
+
+ const {handleDimensionChange} = useVirtualListDimentionChange({cache, listRef});
+
+ const renderRow = ({index, key, style, parent}: ListRowProps) => {
+ const traceRow = items[index];
+
+ return (
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+ {traceRows ? (
+
+ {({width, height}) => (
+ (
+
+ {t('No events recorded')}
+
+ )}
+ overscanRowCount={5}
+ ref={listRef}
+ rowCount={items.length}
+ rowHeight={cache.rowHeight}
+ rowRenderer={renderRow}
+ width={width}
+ />
+ )}
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/static/app/views/replays/detail/perfTable/resizeableContainer.tsx b/static/app/views/replays/detail/perfTable/resizeableContainer.tsx
new file mode 100644
index 00000000000000..80b8c06779754b
--- /dev/null
+++ b/static/app/views/replays/detail/perfTable/resizeableContainer.tsx
@@ -0,0 +1,47 @@
+import type {ReactNode} from 'react';
+import {Fragment} from 'react';
+import styled from '@emotion/styled';
+
+import toPixels from 'sentry/utils/number/toPixels';
+import {useResizableDrawer} from 'sentry/utils/useResizableDrawer';
+import Grabber from 'sentry/views/replays/detail/perfTable/grabber';
+
+interface Props {
+ children: [ReactNode, ReactNode];
+ containerWidth: number;
+ max: number;
+ min: number;
+ onResize: (newSize: number, maybeOldSize?: number | undefined) => void;
+}
+
+function ResizeableContainer({children, containerWidth, min, max, onResize}: Props) {
+ const {isHeld, onDoubleClick, onMouseDown, size} = useResizableDrawer({
+ direction: 'left',
+ initialSize: containerWidth / 2,
+ min,
+ onResize,
+ });
+
+ const leftPx = toPixels(Math.min(size, max));
+ return (
+
+
+ {children}
+
+
+
+
+ );
+}
+
+const ResizeableContainerGrid = styled('div')`
+ display: grid;
+`;
+
+export default ResizeableContainer;
diff --git a/static/app/views/replays/detail/perfTable/traceGrid.tsx b/static/app/views/replays/detail/perfTable/traceGrid.tsx
new file mode 100644
index 00000000000000..6b50c9c066292c
--- /dev/null
+++ b/static/app/views/replays/detail/perfTable/traceGrid.tsx
@@ -0,0 +1,193 @@
+import {Fragment, useRef} from 'react';
+import styled from '@emotion/styled';
+
+import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
+import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
+import {space} from 'sentry/styles/space';
+import {Project} from 'sentry/types';
+import toPercent from 'sentry/utils/number/toPercent';
+import toPixels from 'sentry/utils/number/toPixels';
+import type {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
+import {useDimensions} from 'sentry/utils/useDimensions';
+import useProjects from 'sentry/utils/useProjects';
+import ResizeableContainer from 'sentry/views/replays/detail/perfTable/resizeableContainer';
+import type {FlattenedTrace} from 'sentry/views/replays/detail/perfTable/useReplayPerfData';
+import useVirtualScrolling from 'sentry/views/replays/detail/perfTable/useVirtualScrolling';
+
+const EMDASH = '\u2013';
+
+interface Props {
+ flattenedTrace: FlattenedTrace;
+ onDimensionChange: () => void;
+}
+
+export default function TraceGrid({flattenedTrace, onDimensionChange}: Props) {
+ const measureRef = useRef(null);
+ const {width} = useDimensions({elementRef: measureRef});
+
+ const scrollableWindowRef = useRef(null);
+ const scrollableContentRef = useRef(null);
+ const {offsetX, reclamp: adjustScrollPosition} = useVirtualScrolling({
+ windowRef: scrollableWindowRef,
+ contentRef: scrollableContentRef,
+ });
+
+ const hasSize = width > 0;
+
+ return (
+
+ {hasSize ? (
+ {
+ adjustScrollPosition();
+ onDimensionChange();
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ ) : null}
+
+ );
+}
+
+function SpanNameList({flattenedTrace}: {flattenedTrace: FlattenedTrace}) {
+ const {projects} = useProjects();
+
+ return (
+
+ {flattenedTrace.map(flattened => {
+ const project = projects.find(p => p.id === String(flattened.trace.project_id));
+
+ const labelStyle = {
+ paddingLeft: `calc(${space(2)} * ${flattened.indent})`,
+ };
+
+ return (
+
+
+
+ {flattened.trace['transaction.op']}
+ {EMDASH}
+ {flattened.trace.transaction}
+
+
+ );
+ })}
+
+ );
+}
+
+function SpanDurations({flattenedTrace}: {flattenedTrace: FlattenedTrace}) {
+ const traces = flattenedTrace.map(flattened => flattened.trace);
+ const startTimestampMs = Math.min(...traces.map(trace => trace.start_timestamp)) * 1000;
+ const endTimestampMs = Math.max(
+ ...traces.map(trace => trace.start_timestamp * 1000 + trace['transaction.duration'])
+ );
+
+ return (
+
+ {flattenedTrace.map(flattened => (
+
+
+
+
+
+ {flattened.trace['transaction.duration']}ms
+
+
+ ))}
+
+ );
+}
+
+function barCSSPosition(
+ startTimestampMs: number,
+ endTimestampMs: number,
+ trace: TraceFullDetailed
+) {
+ const fullDuration = Math.abs(endTimestampMs - startTimestampMs) || 1;
+ const sinceStart = trace.start_timestamp * 1000 - startTimestampMs;
+ const duration = trace['transaction.duration'];
+ return {
+ left: toPercent(sinceStart / fullDuration),
+ width: toPercent(duration / fullDuration),
+ };
+}
+
+const TwoColumns = styled('div')`
+ display: grid;
+ grid-template-columns: 1fr max-content;
+`;
+const Relative = styled('div')`
+ position: relative;
+`;
+const OverflowHidden = styled('div')`
+ overflow: hidden;
+`;
+
+const TxnList = styled('div')`
+ font-size: ${p => p.theme.fontSizeRelativeSmall};
+
+ & > :nth-child(2n + 1) {
+ background: ${p => p.theme.backgroundTertiary};
+ }
+`;
+
+const TxnCell = styled('div')`
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-self: auto;
+
+ padding: ${space(0.25)} ${space(0.5)};
+ overflow: hidden;
+`;
+
+const TxnLabel = styled('div')`
+ display: flex;
+ gap: ${space(0.5)};
+
+ align-items: center;
+ white-space: nowrap;
+`;
+
+const TxnDuration = styled('div')`
+ display: flex;
+ flex: 1 1 auto;
+ align-items: center;
+ justify-content: flex-end;
+`;
+
+const TxnDurationBar = styled('div')`
+ position: absolute;
+ content: '';
+ top: 50%;
+ transform: translate(0, -50%);
+ height: ${space(1.5)};
+ margin-block: ${space(0.25)};
+ user-select: none;
+ min-width: 1px;
+`;
diff --git a/static/app/views/replays/detail/perfTable/usePerfFilters.spec.tsx b/static/app/views/replays/detail/perfTable/usePerfFilters.spec.tsx
new file mode 100644
index 00000000000000..631dd05c96e1a7
--- /dev/null
+++ b/static/app/views/replays/detail/perfTable/usePerfFilters.spec.tsx
@@ -0,0 +1,142 @@
+import {browserHistory} from 'react-router';
+import type {Location} from 'history';
+
+import {reactHooks} from 'sentry-test/reactTestingLibrary';
+
+import hydrateBreadcrumbs from 'sentry/utils/replays/hydrateBreadcrumbs';
+import hydrateSpans from 'sentry/utils/replays/hydrateSpans';
+import {LargestContentfulPaintFrame} from 'sentry/utils/replays/types';
+import {useLocation} from 'sentry/utils/useLocation';
+import type {ReplayTraceRow} from 'sentry/views/replays/detail/perfTable/useReplayPerfData';
+
+import usePerfFilters, {FilterFields} from './usePerfFilters';
+
+jest.mock('react-router');
+jest.mock('sentry/utils/useLocation');
+
+const mockUseLocation = jest.mocked(useLocation);
+
+const replayRecord = TestStubs.ReplayRecord();
+
+const CRUMB_1_NAV: ReplayTraceRow = {
+ durationMs: 100,
+ flattenedTraces: [],
+ lcpFrames: hydrateSpans(replayRecord, [
+ TestStubs.Replay.LargestContentfulPaintFrame({
+ startTimestamp: new Date(1663691559961),
+ endTimestamp: new Date(1663691559962),
+ data: {
+ nodeId: 1126,
+ size: 17782,
+ value: 0,
+ },
+ }),
+ ]) as LargestContentfulPaintFrame[],
+ offsetMs: 100,
+ paintFrames: [],
+ replayFrame: hydrateSpans(replayRecord, [
+ TestStubs.Replay.NavigationFrame({
+ startTimestamp: new Date(1663691559961),
+ endTimestamp: new Date(1663691559962),
+ }),
+ ])[0],
+ timestampMs: 1663691559961,
+ traces: [],
+};
+
+const CRUMB_2_CLICK: ReplayTraceRow = {
+ durationMs: 100,
+ flattenedTraces: [],
+ lcpFrames: [],
+ offsetMs: 100,
+ paintFrames: [],
+ replayFrame: hydrateBreadcrumbs(replayRecord, [
+ TestStubs.Replay.ClickFrame({
+ timestamp: new Date(1663691559961),
+ }),
+ ])[0],
+ timestampMs: 1663691560061,
+ traces: [],
+};
+
+describe('usePerfFilters', () => {
+ const traceRows: ReplayTraceRow[] = [CRUMB_1_NAV, CRUMB_2_CLICK];
+
+ beforeEach(() => {
+ jest.mocked(browserHistory.push).mockReset();
+ });
+
+ it('should update the url when setters are called', () => {
+ const TYPE_OPTION = {
+ value: 'ui.click',
+ label: 'User Click',
+ qs: 'f_p_type' as const,
+ };
+
+ mockUseLocation
+ .mockReturnValueOnce({
+ pathname: '/',
+ query: {},
+ } as Location)
+ .mockReturnValueOnce({
+ pathname: '/',
+ query: {f_p_type: [TYPE_OPTION.value]},
+ } as Location);
+
+ const {result} = reactHooks.renderHook(usePerfFilters, {
+ initialProps: {traceRows},
+ });
+
+ result.current.setFilters([TYPE_OPTION]);
+ expect(browserHistory.push).toHaveBeenLastCalledWith({
+ pathname: '/',
+ query: {
+ f_p_type: [TYPE_OPTION.value],
+ },
+ });
+ });
+
+ it('should not filter anything when no values are set', () => {
+ mockUseLocation.mockReturnValue({
+ pathname: '/',
+ query: {},
+ } as Location);
+
+ const {result} = reactHooks.renderHook(usePerfFilters, {initialProps: {traceRows}});
+ expect(result.current.items.length).toEqual(1);
+ });
+
+ it('should filter by crumb type', () => {
+ mockUseLocation.mockReturnValue({
+ pathname: '/',
+ query: {
+ f_p_type: ['ui.click'],
+ },
+ } as Location);
+
+ const {result} = reactHooks.renderHook(usePerfFilters, {initialProps: {traceRows}});
+ expect(result.current.items.length).toEqual(1);
+ });
+});
+
+describe('getCrumbTypes', () => {
+ it('should return a sorted list of crumb types', () => {
+ const traceRows = [CRUMB_1_NAV, CRUMB_2_CLICK]; // ACTION_1_DEBUG, ACTION_2_CLICK];
+
+ const {result} = reactHooks.renderHook(usePerfFilters, {initialProps: {traceRows}});
+ expect(result.current.getCrumbTypes()).toStrictEqual([
+ {label: 'Page Load', qs: 'f_p_type', value: 'navigation.navigate'},
+ {label: 'User Click', qs: 'f_p_type', value: 'ui.click'},
+ ]);
+ });
+
+ it('should deduplicate crumb types', () => {
+ const traceRows = [CRUMB_1_NAV, CRUMB_2_CLICK, CRUMB_2_CLICK];
+
+ const {result} = reactHooks.renderHook(usePerfFilters, {initialProps: {traceRows}});
+ expect(result.current.getCrumbTypes()).toStrictEqual([
+ {label: 'Page Load', qs: 'f_p_type', value: 'navigation.navigate'},
+ {label: 'User Click', qs: 'f_p_type', value: 'ui.click'},
+ ]);
+ });
+});
diff --git a/static/app/views/replays/detail/perfTable/usePerfFilters.tsx b/static/app/views/replays/detail/perfTable/usePerfFilters.tsx
new file mode 100644
index 00000000000000..326b5482515666
--- /dev/null
+++ b/static/app/views/replays/detail/perfTable/usePerfFilters.tsx
@@ -0,0 +1,102 @@
+import {useCallback, useMemo} from 'react';
+import uniq from 'lodash/uniq';
+
+import type {SelectOption} from 'sentry/components/compactSelect';
+import {decodeList} from 'sentry/utils/queryString';
+import useFiltersInLocationQuery from 'sentry/utils/replays/hooks/useFiltersInLocationQuery';
+import {getFrameOpOrCategory} from 'sentry/utils/replays/types';
+import type {ReplayTraceRow} from 'sentry/views/replays/detail/perfTable/useReplayPerfData';
+import {filterItems} from 'sentry/views/replays/detail/utils';
+
+export interface PerfSelectOption extends SelectOption {
+ qs: 'f_p_type';
+}
+
+const DEFAULT_FILTERS = {
+ f_p_type: [],
+} as Record;
+
+export type FilterFields = {
+ f_p_type: string[];
+};
+
+type Options = {
+ traceRows: ReplayTraceRow[];
+};
+
+type Return = {
+ getCrumbTypes: () => {label: string; value: string}[];
+ items: ReplayTraceRow[];
+ selectValue: string[];
+ setFilters: (val: PerfSelectOption[]) => void;
+};
+
+const TYPE_TO_LABEL: Record = {
+ 'ui.click': 'User Click',
+ 'ui.slowClickDetected': 'Rage & Dead Click',
+ 'navigation.back_forward': 'Navigate Back/Forward',
+ 'navigation.navigate': 'Page Load',
+ 'navigation.push': 'Navigation',
+ 'navigation.reload': 'Reload',
+};
+
+function typeToLabel(val: string): string {
+ return TYPE_TO_LABEL[val] ?? 'Unknown';
+}
+
+const FILTERS = {
+ type: (item: ReplayTraceRow, type: string[]) =>
+ type.length === 0 || type.includes(getFrameOpOrCategory(item.replayFrame)),
+};
+
+function usePerfFilters({traceRows}: Options): Return {
+ const {setFilter, query} = useFiltersInLocationQuery();
+
+ const type = useMemo(() => decodeList(query.f_p_type), [query.f_p_type]);
+
+ const items = useMemo(
+ () =>
+ filterItems({
+ items: traceRows,
+ filterFns: FILTERS,
+ filterVals: {type},
+ }),
+ [traceRows, type]
+ );
+
+ const getCrumbTypes = useCallback(
+ () =>
+ uniq(
+ traceRows.map(traceRow => getFrameOpOrCategory(traceRow.replayFrame)).concat(type)
+ )
+ .sort()
+ .map(value => ({
+ value,
+ label: typeToLabel(value),
+ qs: 'f_p_type',
+ })),
+ [traceRows, type]
+ );
+
+ const setFilters = useCallback(
+ (value: PerfSelectOption[]) => {
+ const groupedValues = value.reduce((state, selection) => {
+ return {
+ ...state,
+ [selection.qs]: [...state[selection.qs], selection.value],
+ };
+ }, DEFAULT_FILTERS);
+ setFilter(groupedValues);
+ },
+ [setFilter]
+ );
+
+ return {
+ getCrumbTypes,
+ items,
+ setFilters,
+ selectValue: [...type],
+ };
+}
+
+export default usePerfFilters;
diff --git a/static/app/views/replays/detail/perfTable/useReplayPerfData.tsx b/static/app/views/replays/detail/perfTable/useReplayPerfData.tsx
new file mode 100644
index 00000000000000..c63b2a961525e0
--- /dev/null
+++ b/static/app/views/replays/detail/perfTable/useReplayPerfData.tsx
@@ -0,0 +1,108 @@
+import {useEffect, useState} from 'react';
+
+import type {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
+import type ReplayReader from 'sentry/utils/replays/replayReader';
+import type {
+ LargestContentfulPaintFrame,
+ PaintFrame,
+ ReplayFrame,
+} from 'sentry/utils/replays/types';
+import {
+ useFetchTransactions,
+ useTransactionData,
+} from 'sentry/views/replays/detail/trace/replayTransactionContext';
+
+interface IndentedTraceDetailed {
+ indent: number;
+ trace: TraceFullDetailed;
+}
+
+export type FlattenedTrace = IndentedTraceDetailed[];
+
+export interface ReplayTraceRow {
+ durationMs: number;
+ flattenedTraces: FlattenedTrace[];
+ lcpFrames: LargestContentfulPaintFrame[];
+ offsetMs: number;
+ paintFrames: PaintFrame[];
+ replayFrame: ReplayFrame;
+ timestampMs: number;
+ traces: TraceFullDetailed[];
+}
+
+interface Props {
+ replay: ReplayReader | null;
+}
+
+function mapTraces(indent: number, traces: TraceFullDetailed[]) {
+ return traces.flatMap(trace => {
+ return [
+ {
+ indent,
+ trace,
+ },
+ ...mapTraces(indent + 1, trace.children),
+ ];
+ });
+}
+
+export default function useReplayPerfData({replay}: Props) {
+ const [data, setData] = useState([]);
+
+ const {
+ state: {didInit: _didInit, errors: errors, isFetching: isFetching, traces = []},
+ eventView: _eventView,
+ } = useTransactionData();
+
+ useFetchTransactions();
+
+ useEffect(() => {
+ if (!replay) {
+ return;
+ }
+
+ const frames = replay.getPerfFrames();
+
+ const rows = frames.map((thisFrame, i): ReplayTraceRow => {
+ const nextFrame = frames[i + 1] as ReplayFrame | undefined;
+
+ const isWithinThisAndNextFrame = (frame: ReplayFrame) => {
+ return (
+ frame.timestampMs > thisFrame.timestampMs &&
+ (nextFrame === undefined || frame.timestampMs < nextFrame.timestampMs)
+ );
+ };
+
+ const lcpFrames = replay.getLPCFrames().filter(isWithinThisAndNextFrame);
+ const paintFrames = replay.getPaintFrames().filter(isWithinThisAndNextFrame);
+
+ const tracesAfterThis = traces.filter(
+ trace => trace.timestamp * 1000 >= thisFrame.timestampMs
+ );
+
+ const relatedTraces = nextFrame
+ ? tracesAfterThis.filter(trace => trace.timestamp * 1000 < nextFrame.timestampMs)
+ : tracesAfterThis;
+ const flattenedTraces = relatedTraces.map(trace => mapTraces(0, [trace]));
+
+ return {
+ durationMs: nextFrame ? nextFrame.timestampMs - thisFrame.timestampMs : 0,
+ lcpFrames,
+ offsetMs: thisFrame.offsetMs,
+ paintFrames,
+ replayFrame: thisFrame,
+ timestampMs: thisFrame.timestampMs,
+ traces: relatedTraces,
+ flattenedTraces,
+ };
+ });
+
+ setData(rows);
+ }, [replay, traces]);
+
+ return {
+ data,
+ errors,
+ isFetching,
+ };
+}
diff --git a/static/app/views/replays/detail/perfTable/useVirtualScrolling.tsx b/static/app/views/replays/detail/perfTable/useVirtualScrolling.tsx
new file mode 100644
index 00000000000000..c680da212f5937
--- /dev/null
+++ b/static/app/views/replays/detail/perfTable/useVirtualScrolling.tsx
@@ -0,0 +1,66 @@
+import {RefObject, useCallback, useEffect, useState} from 'react';
+
+import clamp from 'sentry/utils/number/clamp';
+
+interface Props {
+ contentRef: RefObject;
+ windowRef: RefObject;
+}
+
+export default function useVirtualScrolling({
+ contentRef,
+ windowRef,
+}: Props) {
+ const window = windowRef.current;
+ const content = contentRef.current;
+
+ const [scrollPosition, setScrollPosition] = useState({
+ offsetX: 0,
+ offsetY: 0,
+ });
+
+ const reclamp = useCallback(() => {
+ if (!window) {
+ return;
+ }
+ setScrollPosition(prev => {
+ const minX =
+ (content?.clientWidth ?? Number.MAX_SAFE_INTEGER) * -1 + window.clientWidth;
+ const minY =
+ (content?.clientHeight ?? Number.MAX_SAFE_INTEGER) * -1 + window.clientHeight;
+ const offsetX = clamp(prev.offsetX, minX, 0);
+ const offsetY = clamp(prev.offsetY, minY, 0);
+ return {offsetX, offsetY};
+ });
+ }, [content, window]);
+
+ useEffect(() => {
+ if (!window) {
+ return () => {};
+ }
+
+ const handleWheel = (e: WheelEvent) => {
+ const {deltaX, deltaY} = e;
+
+ setScrollPosition(prev => {
+ const minX =
+ (content?.clientWidth ?? Number.MAX_SAFE_INTEGER) * -1 + window.clientWidth;
+ const minY =
+ (content?.clientHeight ?? Number.MAX_SAFE_INTEGER) * -1 + window.clientHeight;
+ const offsetX = clamp(prev.offsetX - deltaX, minX, 0);
+ const offsetY = clamp(prev.offsetY - deltaY, minY, 0);
+ return {offsetX, offsetY};
+ });
+ };
+
+ window.addEventListener('wheel', handleWheel);
+ return () => {
+ window.removeEventListener('wheel', handleWheel);
+ };
+ }, [content, window]);
+
+ return {
+ ...scrollPosition,
+ reclamp,
+ };
+}
diff --git a/static/app/views/replays/detail/useVirtualListDimentionChange.tsx b/static/app/views/replays/detail/useVirtualListDimentionChange.tsx
index 6baf496df4be17..6d778da7724fd1 100644
--- a/static/app/views/replays/detail/useVirtualListDimentionChange.tsx
+++ b/static/app/views/replays/detail/useVirtualListDimentionChange.tsx
@@ -1,24 +1,19 @@
-import {MouseEvent, RefObject, useCallback} from 'react';
+import {RefObject, useCallback} from 'react';
import {CellMeasurerCache, List} from 'react-virtualized';
-import {OnExpandCallback} from 'sentry/components/objectInspector';
-
type Opts = {
cache: CellMeasurerCache;
listRef: RefObject;
};
-export type OnDimensionChange = OnExpandCallback extends (...a: infer U) => infer R
- ? (index: number, ...a: U) => R
- : never;
+export type OnDimensionChange = (index: number) => void;
export default function useVirtualListDimentionChange({cache, listRef}: Opts) {
const handleDimensionChange = useCallback(
- (index: number, event: MouseEvent) => {
+ (index: number) => {
cache.clear(index, 0);
listRef.current?.recomputeGridSize({rowIndex: index});
listRef.current?.forceUpdateGrid();
- event.stopPropagation();
},
[cache, listRef]
);
diff --git a/static/app/views/replays/detail/useVirtualizedInspector.tsx b/static/app/views/replays/detail/useVirtualizedInspector.tsx
index 60ba6586ba825f..f2cafb9684ed5f 100644
--- a/static/app/views/replays/detail/useVirtualizedInspector.tsx
+++ b/static/app/views/replays/detail/useVirtualizedInspector.tsx
@@ -9,6 +9,13 @@ type Opts = {
listRef: RefObject;
};
+export type OnDimensionChange = (
+ index: number,
+ path: string,
+ expandedState: Record,
+ event: MouseEvent
+) => void;
+
export default function useVirtualizedInspector({cache, listRef, expandPathsRef}: Opts) {
const {handleDimensionChange} = useVirtualListDimentionChange({cache, listRef});
@@ -29,7 +36,8 @@ export default function useVirtualizedInspector({cache, listRef, expandPathsRef}
rowState.delete(path);
}
expandPathsRef.current?.set(index, rowState);
- handleDimensionChange(index, event);
+ handleDimensionChange(index);
+ event.stopPropagation();
},
[expandPathsRef, handleDimensionChange]
),
diff --git a/static/app/views/settings/organizationAuditLog/auditLogList.tsx b/static/app/views/settings/organizationAuditLog/auditLogList.tsx
index 2fd15e5c49fe64..99d559a1bdff5d 100644
--- a/static/app/views/settings/organizationAuditLog/auditLogList.tsx
+++ b/static/app/views/settings/organizationAuditLog/auditLogList.tsx
@@ -212,6 +212,20 @@ function AuditNote({
);
}
+ if (entry.event === 'project.ownership-rule.edit') {
+ return (
+
+ {tct('Modified ownership rules in project [projectSettingsLink]', {
+ projectSettingsLink: (
+
+ {entry.data.slug}
+
+ ),
+ })}
+
+ );
+ }
+
return {entry.note};
}
diff --git a/static/app/views/settings/organizationMembers/inviteBanner.spec.tsx b/static/app/views/settings/organizationMembers/inviteBanner.spec.tsx
index d014f5526852ef..9672c5c8269da1 100644
--- a/static/app/views/settings/organizationMembers/inviteBanner.spec.tsx
+++ b/static/app/views/settings/organizationMembers/inviteBanner.spec.tsx
@@ -41,8 +41,10 @@ describe('inviteBanner', function () {
render(
undefined}
+ onSendInvite={() => {}}
organization={org}
+ allowedRoles={[]}
+ onModalClose={() => {}}
/>
);
@@ -63,8 +65,10 @@ describe('inviteBanner', function () {
render(
undefined}
+ onSendInvite={() => {}}
organization={org}
+ allowedRoles={[]}
+ onModalClose={() => {}}
/>
);
@@ -83,8 +87,10 @@ describe('inviteBanner', function () {
render(
undefined}
+ onSendInvite={() => {}}
organization={org}
+ allowedRoles={[]}
+ onModalClose={() => {}}
/>
);
@@ -104,8 +110,10 @@ describe('inviteBanner', function () {
render(
undefined}
+ onSendInvite={() => {}}
organization={org}
+ allowedRoles={[]}
+ onModalClose={() => {}}
/>
);
@@ -136,8 +144,10 @@ describe('inviteBanner', function () {
render(
undefined}
+ onSendInvite={() => {}}
organization={org}
+ allowedRoles={[]}
+ onModalClose={() => {}}
/>
);
@@ -168,8 +178,10 @@ describe('inviteBanner', function () {
render(
undefined}
+ onSendInvite={() => {}}
organization={org}
+ allowedRoles={[]}
+ onModalClose={() => {}}
/>
);
diff --git a/static/app/views/settings/organizationMembers/inviteBanner.tsx b/static/app/views/settings/organizationMembers/inviteBanner.tsx
index b2329a224cc8d3..e18584a3ce0644 100644
--- a/static/app/views/settings/organizationMembers/inviteBanner.tsx
+++ b/static/app/views/settings/organizationMembers/inviteBanner.tsx
@@ -1,6 +1,7 @@
import {useCallback, useEffect, useState} from 'react';
import styled from '@emotion/styled';
+import {openInviteMissingMembersModal} from 'sentry/actionCreators/modal';
import {promptsCheck, promptsUpdate} from 'sentry/actionCreators/prompts';
import {Button} from 'sentry/components/button';
import Card from 'sentry/components/card';
@@ -12,18 +13,26 @@ import QuestionTooltip from 'sentry/components/questionTooltip';
import {IconCommit, IconEllipsis, IconGithub, IconMail} from 'sentry/icons';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
-import {MissingMember, Organization} from 'sentry/types';
+import {MissingMember, Organization, OrgRole} from 'sentry/types';
import {promptIsDismissed} from 'sentry/utils/promptIsDismissed';
import useApi from 'sentry/utils/useApi';
import withOrganization from 'sentry/utils/withOrganization';
type Props = {
+ allowedRoles: OrgRole[];
missingMembers: {integration: string; users: MissingMember[]};
+ onModalClose: () => void;
onSendInvite: (email: string) => void;
organization: Organization;
};
-export function InviteBanner({missingMembers, onSendInvite, organization}: Props) {
+export function InviteBanner({
+ missingMembers,
+ onSendInvite,
+ organization,
+ allowedRoles,
+ onModalClose,
+}: Props) {
// NOTE: this is currently used for Github only
const hideBanner =
@@ -88,34 +97,48 @@ export function InviteBanner({missingMembers, onSendInvite, organization}: Props
const users = missingMembers.users;
- const cards = users.slice(0, 5).map(member => (
-
-
-
-
- {/* TODO: create mapping from integration to lambda external link function */}
-
- {tct('@[externalId]', {externalId: member.externalId})}
-
-
-
-
- {tct('[commitCount] Recent Commits', {commitCount: member.commitCount})}
-
- {member.email}
-
-
-
- ));
+
+
+
+ {/* TODO(cathy): create mapping from integration to lambda external link function */}
+
+ @{username}
+
+
+
+
+ {tct('[commitCount] Recent Commits', {commitCount: member.commitCount})}
+
+ {member.email}
+
+
+
+ );
+ });
- cards.push();
+ cards.push(
+
+ );
return (
@@ -138,7 +161,14 @@ export function InviteBanner({missingMembers, onSendInvite, organization}: Props
@@ -161,30 +191,47 @@ export function InviteBanner({missingMembers, onSendInvite, organization}: Props
export default withOrganization(InviteBanner);
type SeeMoreCardProps = {
- missingUsers: MissingMember[];
+ allowedRoles: OrgRole[];
+ missingMembers: {integration: string; users: MissingMember[]};
+ onModalClose: () => void;
+ organization: Organization;
};
-function SeeMoreCard({missingUsers}: SeeMoreCardProps) {
+function SeeMoreCard({
+ missingMembers,
+ allowedRoles,
+ onModalClose,
+ organization,
+}: SeeMoreCardProps) {
+ const {users} = missingMembers;
+
return (
{tct('See all [missingMembersCount] missing members', {
- missingMembersCount: missingUsers.length,
+ missingMembersCount: users.length,
})}
{tct('Accounting for [totalCommits] recent commits', {
- totalCommits: missingUsers.reduce((acc, curr) => acc + curr.commitCount, 0),
+ totalCommits: users.reduce((acc, curr) => acc + curr.commitCount, 0),
})}
@@ -202,7 +249,6 @@ const StyledCard = styled(Card)`
const CardTitleContainer = styled('div')`
display: flex;
justify-content: space-between;
- margin-bottom: ${space(1)};
`;
const CardTitleContent = styled('div')`
@@ -217,7 +263,7 @@ const CardTitle = styled('h6')`
color: ${p => p.theme.gray400};
`;
-const Subtitle = styled('div')`
+export const Subtitle = styled('div')`
display: flex;
align-items: center;
font-size: ${p => p.theme.fontSizeSmall};
@@ -264,7 +310,7 @@ const MemberCardContentRow = styled('div')`
}
`;
-const StyledExternalLink = styled(ExternalLink)`
+export const StyledExternalLink = styled(ExternalLink)`
font-size: ${p => p.theme.fontSizeMedium};
`;
diff --git a/static/app/views/settings/organizationMembers/organizationMembersList.tsx b/static/app/views/settings/organizationMembers/organizationMembersList.tsx
index ca25a17357aec2..511c2bc737874a 100644
--- a/static/app/views/settings/organizationMembers/organizationMembersList.tsx
+++ b/static/app/views/settings/organizationMembers/organizationMembersList.tsx
@@ -340,6 +340,8 @@ class OrganizationMembersList extends DeprecatedAsyncView {
{({css}) =>
diff --git a/static/app/views/settings/organizationTeams/teamMembersRow.tsx b/static/app/views/settings/organizationTeams/teamMembersRow.tsx
index 33dc225d2cc420..308711fb9f2d22 100644
--- a/static/app/views/settings/organizationTeams/teamMembersRow.tsx
+++ b/static/app/views/settings/organizationTeams/teamMembersRow.tsx
@@ -130,7 +130,7 @@ const RoleSelectWrapper = styled('div')`
export const GRID_TEMPLATE = `
display: grid;
- grid-template-columns: minmax(100px, 1fr) 200px 95px;
+ grid-template-columns: minmax(100px, 1fr) 200px 150px;
gap: ${space(1)};
`;
diff --git a/static/app/views/starfish/components/chart.tsx b/static/app/views/starfish/components/chart.tsx
index b30c5831896059..3623045ded8306 100644
--- a/static/app/views/starfish/components/chart.tsx
+++ b/static/app/views/starfish/components/chart.tsx
@@ -50,15 +50,15 @@ import {
} from 'sentry/utils/discover/fields';
import usePageFilters from 'sentry/utils/usePageFilters';
import useRouter from 'sentry/utils/useRouter';
-import {SpanMetricsFields} from 'sentry/views/starfish/types';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
const STARFISH_CHART_GROUP = 'starfish_chart_group';
export const STARFISH_FIELDS: Record = {
- [SpanMetricsFields.SPAN_DURATION]: {
+ [SpanMetricsField.SPAN_DURATION]: {
outputType: 'duration',
},
- [SpanMetricsFields.SPAN_SELF_TIME]: {
+ [SpanMetricsField.SPAN_SELF_TIME]: {
outputType: 'duration',
},
// local is only used with `time_spent_percentage` function
diff --git a/static/app/views/starfish/components/chartPanel.tsx b/static/app/views/starfish/components/chartPanel.tsx
index ab6ac6fd208d08..76001354f63e4e 100644
--- a/static/app/views/starfish/components/chartPanel.tsx
+++ b/static/app/views/starfish/components/chartPanel.tsx
@@ -3,14 +3,16 @@ import styled from '@emotion/styled';
import Panel from 'sentry/components/panels/panel';
import PanelBody from 'sentry/components/panels/panelBody';
import {space} from 'sentry/styles/space';
+import {Subtitle} from 'sentry/views/performance/landing/widgets/widgets/singleFieldAreaWidget';
type Props = {
children: React.ReactNode;
button?: JSX.Element;
+ subtitle?: React.ReactNode;
title?: React.ReactNode;
};
-export default function ChartPanel({title, children, button}: Props) {
+export default function ChartPanel({title, children, button, subtitle}: Props) {
return (
@@ -20,19 +22,27 @@ export default function ChartPanel({title, children, button}: Props) {
{button}
)}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
{children}
);
}
-const ChartLabel = styled('p')`
+const SubtitleContainer = styled('div')`
+ padding-top: ${space(0.5)};
+`;
+
+const ChartLabel = styled('div')`
${p => p.theme.text.cardTitle}
`;
const Header = styled('div')`
padding: 0 ${space(1)} 0 0;
- min-height: 36px;
width: 100%;
display: flex;
align-items: center;
diff --git a/static/app/views/starfish/components/spanDescription.tsx b/static/app/views/starfish/components/spanDescription.tsx
index 7bf7c1646cfe9a..31389272f1d0f5 100644
--- a/static/app/views/starfish/components/spanDescription.tsx
+++ b/static/app/views/starfish/components/spanDescription.tsx
@@ -1,22 +1,22 @@
import styled from '@emotion/styled';
import {CodeSnippet} from 'sentry/components/codeSnippet';
-import {SpanMetricsFields, SpanMetricsFieldTypes} from 'sentry/views/starfish/types';
+import {SpanMetricsField, SpanMetricsFieldTypes} from 'sentry/views/starfish/types';
import {SQLishFormatter} from 'sentry/views/starfish/utils/sqlish/SQLishFormatter';
type Props = {
span: Pick<
SpanMetricsFieldTypes,
- SpanMetricsFields.SPAN_OP | SpanMetricsFields.SPAN_DESCRIPTION
+ SpanMetricsField.SPAN_OP | SpanMetricsField.SPAN_DESCRIPTION
>;
};
export function SpanDescription({span}: Props) {
- if (span[SpanMetricsFields.SPAN_OP].startsWith('db')) {
+ if (span[SpanMetricsField.SPAN_OP].startsWith('db')) {
return ;
}
- return {span[SpanMetricsFields.SPAN_DESCRIPTION]};
+ return {span[SpanMetricsField.SPAN_DESCRIPTION]};
}
function DatabaseSpanDescription({span}: Props) {
@@ -24,7 +24,7 @@ function DatabaseSpanDescription({span}: Props) {
return (
- {formatter.toString(span[SpanMetricsFields.SPAN_DESCRIPTION])}
+ {formatter.toString(span[SpanMetricsField.SPAN_DESCRIPTION])}
);
}
diff --git a/static/app/views/starfish/components/tableCells/renderHeadCell.tsx b/static/app/views/starfish/components/tableCells/renderHeadCell.tsx
index d9797013c8a6d5..6e9e6ffeae2859 100644
--- a/static/app/views/starfish/components/tableCells/renderHeadCell.tsx
+++ b/static/app/views/starfish/components/tableCells/renderHeadCell.tsx
@@ -8,7 +8,7 @@ import {
parseFunction,
Sort,
} from 'sentry/utils/discover/fields';
-import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types';
+import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types';
import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
type Options = {
@@ -17,8 +17,8 @@ type Options = {
sort?: Sort;
};
-const {SPAN_SELF_TIME} = SpanMetricsFields;
-const {TIME_SPENT_PERCENTAGE, SPS, SPM, HTTP_ERROR_COUNT} = StarfishFunctions;
+const {SPAN_SELF_TIME} = SpanMetricsField;
+const {TIME_SPENT_PERCENTAGE, SPS, SPM, HTTP_ERROR_COUNT} = SpanFunction;
export const SORTABLE_FIELDS = new Set([
`avg(${SPAN_SELF_TIME})`,
@@ -54,7 +54,7 @@ export const renderHeadCell = ({column, location, sort}: Options) => {
...location,
query: {
...location?.query,
- [QueryParameterNames.SORT]: newSort,
+ [QueryParameterNames.SPANS_SORT]: newSort,
},
};
}}
diff --git a/static/app/views/starfish/components/tableCells/spanDescriptionCell.tsx b/static/app/views/starfish/components/tableCells/spanDescriptionCell.tsx
index 892a6a5c8ba845..3db287baa33030 100644
--- a/static/app/views/starfish/components/tableCells/spanDescriptionCell.tsx
+++ b/static/app/views/starfish/components/tableCells/spanDescriptionCell.tsx
@@ -13,7 +13,7 @@ import {useHoverOverlay, UseHoverOverlayProps} from 'sentry/utils/useHoverOverla
import {useLocation} from 'sentry/utils/useLocation';
import {OverflowEllipsisTextContainer} from 'sentry/views/starfish/components/textAlign';
import {useFullSpanFromTrace} from 'sentry/views/starfish/queries/useFullSpanFromTrace';
-import {ModuleName, StarfishFunctions} from 'sentry/views/starfish/types';
+import {ModuleName, SpanFunction} from 'sentry/views/starfish/types';
import {extractRoute} from 'sentry/views/starfish/utils/extractRoute';
import {useRoutingContext} from 'sentry/views/starfish/utils/routingContext';
import {SQLishFormatter} from 'sentry/views/starfish/utils/sqlish/SQLishFormatter';
@@ -54,13 +54,13 @@ export function SpanDescriptionCell({
endpointMethod,
};
- const sort: string | undefined = queryString[QueryParameterNames.SORT];
+ const sort: string | undefined = queryString[QueryParameterNames.SPANS_SORT];
// the spans page uses time_spent_percentage(local), so to persist the sort upon navigation we need to replace
- if (sort?.includes(`${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`)) {
- queryString[QueryParameterNames.SORT] = sort.replace(
- `${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`,
- `${StarfishFunctions.TIME_SPENT_PERCENTAGE}(local)`
+ if (sort?.includes(`${SpanFunction.TIME_SPENT_PERCENTAGE}()`)) {
+ queryString[QueryParameterNames.SPANS_SORT] = sort.replace(
+ `${SpanFunction.TIME_SPENT_PERCENTAGE}()`,
+ `${SpanFunction.TIME_SPENT_PERCENTAGE}(local)`
);
}
diff --git a/static/app/views/starfish/queries/useFullSpanFromTrace.tsx b/static/app/views/starfish/queries/useFullSpanFromTrace.tsx
index 6e67aef4112bca..8c3fc9d4ec36f3 100644
--- a/static/app/views/starfish/queries/useFullSpanFromTrace.tsx
+++ b/static/app/views/starfish/queries/useFullSpanFromTrace.tsx
@@ -1,6 +1,6 @@
import {useEventJSON} from 'sentry/views/starfish/queries/useEventJSON';
import {useIndexedSpans} from 'sentry/views/starfish/queries/useIndexedSpans';
-import {SpanIndexedFields} from 'sentry/views/starfish/types';
+import {SpanIndexedField} from 'sentry/views/starfish/types';
// NOTE: Fetching the top one is a bit naive, but works for now. A better
// approach might be to fetch several at a time, and let the hook consumer
@@ -9,7 +9,7 @@ export function useFullSpanFromTrace(group?: string, enabled: boolean = true) {
const filters: {[key: string]: string} = {};
if (group) {
- filters[SpanIndexedFields.SPAN_GROUP] = group;
+ filters[SpanIndexedField.SPAN_GROUP] = group;
}
const indexedSpansResponse = useIndexedSpans(filters, 1, enabled);
@@ -17,12 +17,12 @@ export function useFullSpanFromTrace(group?: string, enabled: boolean = true) {
const firstIndexedSpan = indexedSpansResponse.data?.[0];
const eventJSONResponse = useEventJSON(
- firstIndexedSpan ? firstIndexedSpan[SpanIndexedFields.TRANSACTION_ID] : undefined,
- firstIndexedSpan ? firstIndexedSpan[SpanIndexedFields.PROJECT] : undefined
+ firstIndexedSpan ? firstIndexedSpan[SpanIndexedField.TRANSACTION_ID] : undefined,
+ firstIndexedSpan ? firstIndexedSpan[SpanIndexedField.PROJECT] : undefined
);
const fullSpan = eventJSONResponse?.data?.spans?.find(
- span => span.span_id === firstIndexedSpan?.[SpanIndexedFields.ID]
+ span => span.span_id === firstIndexedSpan?.[SpanIndexedField.ID]
);
// N.B. There isn't a great pattern for us to merge the responses together,
diff --git a/static/app/views/starfish/queries/useIndexedSpans.tsx b/static/app/views/starfish/queries/useIndexedSpans.tsx
index 8fd30e5877f55b..aa83ad59945097 100644
--- a/static/app/views/starfish/queries/useIndexedSpans.tsx
+++ b/static/app/views/starfish/queries/useIndexedSpans.tsx
@@ -4,7 +4,7 @@ import EventView from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useLocation} from 'sentry/utils/useLocation';
-import {SpanIndexedFields, SpanIndexedFieldTypes} from 'sentry/views/starfish/types';
+import {SpanIndexedField, SpanIndexedFieldTypes} from 'sentry/views/starfish/types';
import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
const DEFAULT_LIMIT = 10;
@@ -43,7 +43,7 @@ function getEventView(filters: Filters, location: Location) {
{
name: '',
query: search.formatString(),
- fields: Object.values(SpanIndexedFields),
+ fields: Object.values(SpanIndexedField),
dataset: DiscoverDatasets.SPANS_INDEXED,
version: 2,
},
diff --git a/static/app/views/starfish/queries/useSpanList.tsx b/static/app/views/starfish/queries/useSpanList.tsx
index 29823b9b9a7143..0801cf391c9315 100644
--- a/static/app/views/starfish/queries/useSpanList.tsx
+++ b/static/app/views/starfish/queries/useSpanList.tsx
@@ -6,12 +6,12 @@ import EventView from 'sentry/utils/discover/eventView';
import type {Sort} from 'sentry/utils/discover/fields';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {useLocation} from 'sentry/utils/useLocation';
-import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types';
+import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types';
import {buildEventViewQuery} from 'sentry/views/starfish/utils/buildEventViewQuery';
import {useWrappedDiscoverQuery} from 'sentry/views/starfish/utils/useSpansQuery';
const {SPAN_SELF_TIME, SPAN_DESCRIPTION, SPAN_GROUP, SPAN_OP, SPAN_DOMAIN, PROJECT_ID} =
- SpanMetricsFields;
+ SpanMetricsField;
export type SpanMetrics = {
'avg(span.self_time)': number;
diff --git a/static/app/views/starfish/queries/useSpanMetrics.tsx b/static/app/views/starfish/queries/useSpanMetrics.tsx
index 42032da590e41d..c00cbe3fcac155 100644
--- a/static/app/views/starfish/queries/useSpanMetrics.tsx
+++ b/static/app/views/starfish/queries/useSpanMetrics.tsx
@@ -3,10 +3,10 @@ import {Location} from 'history';
import EventView from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {useLocation} from 'sentry/utils/useLocation';
-import {SpanMetricsFields} from 'sentry/views/starfish/types';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
-const {SPAN_GROUP} = SpanMetricsFields;
+const {SPAN_GROUP} = SpanMetricsField;
export type SpanMetrics = {
[metric: string]: number | string;
diff --git a/static/app/views/starfish/queries/useSpanMetricsSeries.tsx b/static/app/views/starfish/queries/useSpanMetricsSeries.tsx
index abe234a9abf85d..2b5f4875d3af30 100644
--- a/static/app/views/starfish/queries/useSpanMetricsSeries.tsx
+++ b/static/app/views/starfish/queries/useSpanMetricsSeries.tsx
@@ -7,11 +7,11 @@ import EventView from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import usePageFilters from 'sentry/utils/usePageFilters';
import {SpanSummaryQueryFilters} from 'sentry/views/starfish/queries/useSpanMetrics';
-import {SpanMetricsFields} from 'sentry/views/starfish/types';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/starfish/utils/constants';
import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
-const {SPAN_GROUP} = SpanMetricsFields;
+const {SPAN_GROUP} = SpanMetricsField;
export type SpanMetrics = {
interval: number;
diff --git a/static/app/views/starfish/queries/useSpanSamples.tsx b/static/app/views/starfish/queries/useSpanSamples.tsx
index 19287ce89e3fc8..87918a29743ab4 100644
--- a/static/app/views/starfish/queries/useSpanSamples.tsx
+++ b/static/app/views/starfish/queries/useSpanSamples.tsx
@@ -9,11 +9,11 @@ import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';
import {computeAxisMax} from 'sentry/views/starfish/components/chart';
import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useSpanMetricsSeries';
-import {SpanIndexedFields, SpanIndexedFieldTypes} from 'sentry/views/starfish/types';
+import {SpanIndexedField, SpanIndexedFieldTypes} from 'sentry/views/starfish/types';
import {getDateConditions} from 'sentry/views/starfish/utils/getDateConditions';
import {DATE_FORMAT} from 'sentry/views/starfish/utils/useSpansQuery';
-const {SPAN_SELF_TIME, SPAN_GROUP} = SpanIndexedFields;
+const {SPAN_SELF_TIME, SPAN_GROUP} = SpanIndexedField;
type Options = {
groupId: string;
@@ -23,11 +23,11 @@ type Options = {
export type SpanSample = Pick<
SpanIndexedFieldTypes,
- | SpanIndexedFields.SPAN_SELF_TIME
- | SpanIndexedFields.TRANSACTION_ID
- | SpanIndexedFields.PROJECT
- | SpanIndexedFields.TIMESTAMP
- | SpanIndexedFields.ID
+ | SpanIndexedField.SPAN_SELF_TIME
+ | SpanIndexedField.TRANSACTION_ID
+ | SpanIndexedField.PROJECT
+ | SpanIndexedField.TIMESTAMP
+ | SpanIndexedField.ID
>;
export const useSpanSamples = (options: Options) => {
diff --git a/static/app/views/starfish/queries/useSpanTransactionMetrics.tsx b/static/app/views/starfish/queries/useSpanTransactionMetrics.tsx
index 727b78baa0fb8e..b3aec798794ea8 100644
--- a/static/app/views/starfish/queries/useSpanTransactionMetrics.tsx
+++ b/static/app/views/starfish/queries/useSpanTransactionMetrics.tsx
@@ -5,10 +5,10 @@ import {Sort} from 'sentry/utils/discover/fields';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useLocation} from 'sentry/utils/useLocation';
-import {SpanMetricsFields} from 'sentry/views/starfish/types';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
import {useWrappedDiscoverQuery} from 'sentry/views/starfish/utils/useSpansQuery';
-const {SPAN_SELF_TIME, SPAN_GROUP} = SpanMetricsFields;
+const {SPAN_SELF_TIME, SPAN_GROUP} = SpanMetricsField;
export type SpanTransactionMetrics = {
'avg(span.self_time)': number;
@@ -24,7 +24,8 @@ export type SpanTransactionMetrics = {
export const useSpanTransactionMetrics = (
group: string,
options: {sorts?: Sort[]; transactions?: string[]},
- _referrer = 'api.starfish.span-transaction-metrics'
+ referrer = 'api.starfish.span-transaction-metrics',
+ cursor?: string
) => {
const location = useLocation();
@@ -37,7 +38,8 @@ export const useSpanTransactionMetrics = (
initialData: [],
enabled: Boolean(group),
limit: 25,
- referrer: _referrer,
+ referrer,
+ cursor,
});
};
diff --git a/static/app/views/starfish/types.tsx b/static/app/views/starfish/types.tsx
index 94bc3703622236..bcc57da0f5a888 100644
--- a/static/app/views/starfish/types.tsx
+++ b/static/app/views/starfish/types.tsx
@@ -16,7 +16,7 @@ export enum ModuleName {
OTHER = 'other',
}
-export enum SpanMetricsFields {
+export enum SpanMetricsField {
SPAN_OP = 'span.op',
SPAN_DESCRIPTION = 'span.description',
SPAN_MODULE = 'span.module',
@@ -29,17 +29,17 @@ export enum SpanMetricsFields {
}
export type SpanMetricsFieldTypes = {
- [SpanMetricsFields.SPAN_OP]: string;
- [SpanMetricsFields.SPAN_DESCRIPTION]: string;
- [SpanMetricsFields.SPAN_MODULE]: string;
- [SpanMetricsFields.SPAN_ACTION]: string;
- [SpanMetricsFields.SPAN_DOMAIN]: string;
- [SpanMetricsFields.SPAN_GROUP]: string;
- [SpanMetricsFields.SPAN_SELF_TIME]: number;
- [SpanMetricsFields.SPAN_DURATION]: number;
+ [SpanMetricsField.SPAN_OP]: string;
+ [SpanMetricsField.SPAN_DESCRIPTION]: string;
+ [SpanMetricsField.SPAN_MODULE]: string;
+ [SpanMetricsField.SPAN_ACTION]: string;
+ [SpanMetricsField.SPAN_DOMAIN]: string;
+ [SpanMetricsField.SPAN_GROUP]: string;
+ [SpanMetricsField.SPAN_SELF_TIME]: number;
+ [SpanMetricsField.SPAN_DURATION]: number;
};
-export enum SpanIndexedFields {
+export enum SpanIndexedField {
SPAN_SELF_TIME = 'span.self_time',
SPAN_GROUP = 'span.group', // Span group computed from the normalized description. Matches the group in the metrics data set
SPAN_GROUP_RAW = 'span.group_raw', // Span group computed from non-normalized description. Matches the group in the event payload
@@ -57,25 +57,25 @@ export enum SpanIndexedFields {
}
export type SpanIndexedFieldTypes = {
- [SpanIndexedFields.SPAN_SELF_TIME]: number;
- [SpanIndexedFields.SPAN_GROUP]: string;
- [SpanIndexedFields.SPAN_GROUP_RAW]: string;
- [SpanIndexedFields.SPAN_MODULE]: string;
- [SpanIndexedFields.SPAN_DESCRIPTION]: string;
- [SpanIndexedFields.SPAN_OP]: string;
- [SpanIndexedFields.ID]: string;
- [SpanIndexedFields.SPAN_ACTION]: string;
- [SpanIndexedFields.TRANSACTION_ID]: string;
- [SpanIndexedFields.TRANSACTION_METHOD]: string;
- [SpanIndexedFields.TRANSACTION_OP]: string;
- [SpanIndexedFields.SPAN_DOMAIN]: string;
- [SpanIndexedFields.TIMESTAMP]: string;
- [SpanIndexedFields.PROJECT]: string;
+ [SpanIndexedField.SPAN_SELF_TIME]: number;
+ [SpanIndexedField.SPAN_GROUP]: string;
+ [SpanIndexedField.SPAN_GROUP_RAW]: string;
+ [SpanIndexedField.SPAN_MODULE]: string;
+ [SpanIndexedField.SPAN_DESCRIPTION]: string;
+ [SpanIndexedField.SPAN_OP]: string;
+ [SpanIndexedField.ID]: string;
+ [SpanIndexedField.SPAN_ACTION]: string;
+ [SpanIndexedField.TRANSACTION_ID]: string;
+ [SpanIndexedField.TRANSACTION_METHOD]: string;
+ [SpanIndexedField.TRANSACTION_OP]: string;
+ [SpanIndexedField.SPAN_DOMAIN]: string;
+ [SpanIndexedField.TIMESTAMP]: string;
+ [SpanIndexedField.PROJECT]: string;
};
-export type Op = SpanIndexedFieldTypes[SpanIndexedFields.SPAN_OP];
+export type Op = SpanIndexedFieldTypes[SpanIndexedField.SPAN_OP];
-export enum StarfishFunctions {
+export enum SpanFunction {
SPS = 'sps',
SPM = 'spm',
SPS_PERCENENT_CHANGE = 'sps_percent_change',
@@ -84,39 +84,39 @@ export enum StarfishFunctions {
}
export const StarfishDatasetFields = {
- [DiscoverDatasets.SPANS_METRICS]: SpanIndexedFields,
- [DiscoverDatasets.SPANS_INDEXED]: SpanIndexedFields,
+ [DiscoverDatasets.SPANS_METRICS]: SpanIndexedField,
+ [DiscoverDatasets.SPANS_INDEXED]: SpanIndexedField,
};
export const STARFISH_AGGREGATION_FIELDS: Record<
- StarfishFunctions,
+ SpanFunction,
FieldDefinition & {defaultOutputType: AggregationOutputType}
> = {
- [StarfishFunctions.SPS]: {
+ [SpanFunction.SPS]: {
desc: t('Spans per second'),
kind: FieldKind.FUNCTION,
defaultOutputType: 'number',
valueType: FieldValueType.NUMBER,
},
- [StarfishFunctions.SPM]: {
+ [SpanFunction.SPM]: {
desc: t('Spans per minute'),
kind: FieldKind.FUNCTION,
defaultOutputType: 'number',
valueType: FieldValueType.NUMBER,
},
- [StarfishFunctions.TIME_SPENT_PERCENTAGE]: {
+ [SpanFunction.TIME_SPENT_PERCENTAGE]: {
desc: t('Span time spent percentage'),
defaultOutputType: 'percentage',
kind: FieldKind.FUNCTION,
valueType: FieldValueType.NUMBER,
},
- [StarfishFunctions.HTTP_ERROR_COUNT]: {
+ [SpanFunction.HTTP_ERROR_COUNT]: {
desc: t('Count of 5XX http errors'),
defaultOutputType: 'integer',
kind: FieldKind.FUNCTION,
valueType: FieldValueType.NUMBER,
},
- [StarfishFunctions.SPS_PERCENENT_CHANGE]: {
+ [SpanFunction.SPS_PERCENENT_CHANGE]: {
desc: t('Spans per second percentage change'),
defaultOutputType: 'percentage',
kind: FieldKind.FUNCTION,
diff --git a/static/app/views/starfish/utils/buildEventViewQuery.tsx b/static/app/views/starfish/utils/buildEventViewQuery.tsx
index 917499c76fca14..ee4f7ab27e7a05 100644
--- a/static/app/views/starfish/utils/buildEventViewQuery.tsx
+++ b/static/app/views/starfish/utils/buildEventViewQuery.tsx
@@ -1,12 +1,12 @@
import {Location} from 'history';
import {defined} from 'sentry/utils';
-import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types';
+import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types';
import {EMPTY_OPTION_VALUE} from 'sentry/views/starfish/views/spans/selectors/emptyOption';
import {NULL_SPAN_CATEGORY} from 'sentry/views/starfish/views/webServiceView/spanGroupBreakdownContainer';
const {SPAN_DESCRIPTION, SPAN_OP, SPAN_DOMAIN, SPAN_ACTION, SPAN_MODULE} =
- SpanMetricsFields;
+ SpanMetricsField;
const SPAN_FILTER_KEYS = [
SPAN_OP,
diff --git a/static/app/views/starfish/utils/sqlish/SQLishParser.spec.tsx b/static/app/views/starfish/utils/sqlish/SQLishParser.spec.tsx
index 7754f4499e287a..a53ee0c93008a7 100644
--- a/static/app/views/starfish/utils/sqlish/SQLishParser.spec.tsx
+++ b/static/app/views/starfish/utils/sqlish/SQLishParser.spec.tsx
@@ -26,6 +26,7 @@ describe('SQLishParser', function () {
'flags | %s)', // Bitwise OR
'flags ^ %s)', // Bitwise XOR
'flags ~ %s)', // Bitwise NOT
+ '+ %s as count', // Arithmetic
])('Parses %s', sql => {
expect(() => {
parser.parse(sql);
diff --git a/static/app/views/starfish/utils/sqlish/sqlish.pegjs b/static/app/views/starfish/utils/sqlish/sqlish.pegjs
index 3691ab00023491..a6f08d9c876690 100644
--- a/static/app/views/starfish/utils/sqlish/sqlish.pegjs
+++ b/static/app/views/starfish/utils/sqlish/sqlish.pegjs
@@ -36,4 +36,4 @@ Whitespace
= Whitespace:[\n\t ]+ { return { type: 'Whitespace', content: Whitespace.join("") } }
GenericToken
- = GenericToken:[a-zA-Z0-9"'`_\-.=><:,*;!\[\]?$%|/@&~^]+ { return { type: 'GenericToken', content: GenericToken.join('') } }
+ = GenericToken:[a-zA-Z0-9"'`_\-.=><:,*;!\[\]?$%|/@&~^+]+ { return { type: 'GenericToken', content: GenericToken.join('') } }
diff --git a/static/app/views/starfish/views/queryParameters.tsx b/static/app/views/starfish/views/queryParameters.tsx
index f5371bef3c1362..fbce6a697b4056 100644
--- a/static/app/views/starfish/views/queryParameters.tsx
+++ b/static/app/views/starfish/views/queryParameters.tsx
@@ -1,4 +1,5 @@
export enum QueryParameterNames {
- CURSOR = 'spansCursor',
- SORT = 'spansSort',
+ SPANS_CURSOR = 'spansCursor',
+ SPANS_SORT = 'spansSort',
+ ENDPOINTS_CURSOR = 'endpointsCursor',
}
diff --git a/static/app/views/starfish/views/spanSummaryPage/index.tsx b/static/app/views/starfish/views/spanSummaryPage/index.tsx
index a03d0ae24042a8..1c800d10111218 100644
--- a/static/app/views/starfish/views/spanSummaryPage/index.tsx
+++ b/static/app/views/starfish/views/spanSummaryPage/index.tsx
@@ -19,7 +19,7 @@ import {
SpanSummaryQueryFilters,
useSpanMetrics,
} from 'sentry/views/starfish/queries/useSpanMetrics';
-import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types';
+import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types';
import {extractRoute} from 'sentry/views/starfish/utils/extractRoute';
import {ROUTE_NAMES} from 'sentry/views/starfish/utils/routeNames';
import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
@@ -36,7 +36,7 @@ type Query = {
endpointMethod: string;
transaction: string;
transactionMethod: string;
- [QueryParameterNames.SORT]: string;
+ [QueryParameterNames.SPANS_SORT]: string;
};
type Props = {
@@ -54,7 +54,7 @@ function SpanSummaryPage({params, location}: Props) {
: {};
const sort =
- fromSorts(location.query[QueryParameterNames.SORT]).filter(isAValidSort)[0] ??
+ fromSorts(location.query[QueryParameterNames.SPANS_SORT]).filter(isAValidSort)[0] ??
DEFAULT_SORT; // We only allow one sort on this table in this view
if (endpointMethod && queryFilter) {
@@ -65,21 +65,21 @@ function SpanSummaryPage({params, location}: Props) {
groupId,
queryFilter,
[
- SpanMetricsFields.SPAN_OP,
- SpanMetricsFields.SPAN_GROUP,
- SpanMetricsFields.PROJECT_ID,
- `${StarfishFunctions.SPS}()`,
+ SpanMetricsField.SPAN_OP,
+ SpanMetricsField.SPAN_GROUP,
+ SpanMetricsField.PROJECT_ID,
+ `${SpanFunction.SPS}()`,
],
'api.starfish.span-summary-page-metrics'
);
const span = {
- [SpanMetricsFields.SPAN_OP]: spanMetrics[SpanMetricsFields.SPAN_OP],
- [SpanMetricsFields.SPAN_GROUP]: groupId,
+ [SpanMetricsField.SPAN_OP]: spanMetrics[SpanMetricsField.SPAN_OP],
+ [SpanMetricsField.SPAN_GROUP]: groupId,
};
const title = [
- getSpanOperationDescription(span[SpanMetricsFields.SPAN_OP]),
+ getSpanOperationDescription(span[SpanMetricsField.SPAN_OP]),
t('Summary'),
].join(' ');
@@ -137,8 +137,7 @@ function SpanSummaryPage({params, location}: Props) {
)}
diff --git a/static/app/views/starfish/views/spanSummaryPage/sampleList/durationChart/index.tsx b/static/app/views/starfish/views/spanSummaryPage/sampleList/durationChart/index.tsx
index 75f9831152faac..48c81250db07ce 100644
--- a/static/app/views/starfish/views/spanSummaryPage/sampleList/durationChart/index.tsx
+++ b/static/app/views/starfish/views/spanSummaryPage/sampleList/durationChart/index.tsx
@@ -3,21 +3,22 @@ import {useTheme} from '@emotion/react';
import {t} from 'sentry/locale';
import {EChartClickHandler, EChartHighlightHandler, Series} from 'sentry/types/echarts';
import {usePageError} from 'sentry/utils/performance/contexts/pageError';
+import usePageFilters from 'sentry/utils/usePageFilters';
import {AVG_COLOR} from 'sentry/views/starfish/colours';
import Chart from 'sentry/views/starfish/components/chart';
+import ChartPanel from 'sentry/views/starfish/components/chartPanel';
import {isNearAverage} from 'sentry/views/starfish/components/samplesTable/common';
import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useSpanMetricsSeries';
import {SpanSample, useSpanSamples} from 'sentry/views/starfish/queries/useSpanSamples';
-import {SpanMetricsFields} from 'sentry/views/starfish/types';
-import {DataTitles} from 'sentry/views/starfish/views/spans/types';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
import {
crossIconPath,
downwardPlayIconPath,
upwardPlayIconPath,
} from 'sentry/views/starfish/views/spanSummaryPage/sampleList/durationChart/symbol';
-const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsFields;
+const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsField;
type Props = {
groupId: string;
@@ -41,6 +42,7 @@ function DurationChart({
}: Props) {
const theme = useTheme();
const {setPageError} = usePageError();
+ const pageFilter = usePageFilters();
const getSampleSymbol = (
duration: number,
@@ -176,27 +178,32 @@ function DurationChart({
setPageError(t('An error has occured while loading chart data'));
}
+ const subtitle = pageFilter.selection.datetime.period
+ ? t('Last %s', pageFilter.selection.datetime.period)
+ : t('Last period');
+
return (
-
-
{DataTitles.avg}
-
-
+
+
+
+
+
);
}
diff --git a/static/app/views/starfish/views/spanSummaryPage/sampleList/index.tsx b/static/app/views/starfish/views/spanSummaryPage/sampleList/index.tsx
index 8608e4f0b3bddf..3d47df4979869d 100644
--- a/static/app/views/starfish/views/spanSummaryPage/sampleList/index.tsx
+++ b/static/app/views/starfish/views/spanSummaryPage/sampleList/index.tsx
@@ -23,17 +23,11 @@ import SampleTable from 'sentry/views/starfish/views/spanSummaryPage/sampleList/
type Props = {
groupId: string;
- projectId: number;
transactionMethod: string;
transactionName: string;
};
-export function SampleList({
- groupId,
- projectId,
- transactionName,
- transactionMethod,
-}: Props) {
+export function SampleList({groupId, transactionName, transactionMethod}: Props) {
const router = useRouter();
const [highlightedSpanId, setHighlightedSpanId] = useState(
undefined
@@ -56,8 +50,8 @@ export function SampleList({
const {projects} = useProjects();
const project = useMemo(
- () => projects.find(p => p.id === String(projectId)),
- [projects, projectId]
+ () => projects.find(p => p.id === String(query.project)),
+ [projects, query.project]
);
const onOpenDetailPanel = useCallback(() => {
@@ -72,7 +66,7 @@ export function SampleList({
: transactionName;
const link = `/performance/summary/?${qs.stringify({
- project: projectId,
+ project: query.project,
transaction: transactionName,
})}`;
diff --git a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleInfo/index.tsx b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleInfo/index.tsx
index 4a0b6e549d1a10..47a3761e063b6e 100644
--- a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleInfo/index.tsx
+++ b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleInfo/index.tsx
@@ -6,11 +6,11 @@ import {usePageError} from 'sentry/utils/performance/contexts/pageError';
import {DurationCell} from 'sentry/views/starfish/components/tableCells/durationCell';
import {ThroughputCell} from 'sentry/views/starfish/components/tableCells/throughputCell';
import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
-import {SpanMetricsFields} from 'sentry/views/starfish/types';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
import {DataTitles, getThroughputTitle} from 'sentry/views/starfish/views/spans/types';
import {Block, BlockContainer} from 'sentry/views/starfish/views/spanSummaryPage/block';
-const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsFields;
+const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsField;
type Props = {
groupId: string;
diff --git a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx
index fa640c0c3710bd..9c3ae44864adc3 100644
--- a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx
+++ b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx
@@ -5,7 +5,7 @@ import {
} from 'sentry-test/reactTestingLibrary';
import {PageFilters} from 'sentry/types';
-import {SpanMetricsFields} from 'sentry/views/starfish/types';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
import SampleTable from './sampleTable';
@@ -94,8 +94,8 @@ const initializeMockRequests = () => {
body: {
data: [
{
- [SpanMetricsFields.SPAN_OP]: 'db',
- [`avg(${SpanMetricsFields.SPAN_SELF_TIME})`]: 0.52,
+ [SpanMetricsField.SPAN_OP]: 'db',
+ [`avg(${SpanMetricsField.SPAN_SELF_TIME})`]: 0.52,
},
],
},
diff --git a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.tsx b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.tsx
index bcf3f64423de3f..0e5f80347b2679 100644
--- a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.tsx
+++ b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.tsx
@@ -13,9 +13,9 @@ import {SpanSamplesTable} from 'sentry/views/starfish/components/samplesTable/sp
import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
import {SpanSample, useSpanSamples} from 'sentry/views/starfish/queries/useSpanSamples';
import {useTransactions} from 'sentry/views/starfish/queries/useTransactions';
-import {SpanMetricsFields} from 'sentry/views/starfish/types';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
-const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsFields;
+const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsField;
const SpanSamplesTableContainer = styled('div')`
padding-bottom: ${space(2)};
diff --git a/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.spec.tsx b/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.spec.tsx
index 90fca50a0c2812..a215936e1144cb 100644
--- a/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.spec.tsx
+++ b/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.spec.tsx
@@ -1,15 +1,15 @@
import {render, screen} from 'sentry-test/reactTestingLibrary';
-import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types';
+import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types';
import {SpanMetricsRibbon} from 'sentry/views/starfish/views/spanSummaryPage/spanMetricsRibbon';
describe('SpanMetricsRibbon', function () {
const sampleMetrics = {
- [SpanMetricsFields.SPAN_OP]: 'db',
- [`${StarfishFunctions.SPM}()`]: 17.8,
- [`avg(${SpanMetricsFields.SPAN_SELF_TIME})`]: 127.1,
- [`sum(${SpanMetricsFields.SPAN_SELF_TIME})`]: 1172319,
- [`${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`]: 0.002,
+ [SpanMetricsField.SPAN_OP]: 'db',
+ [`${SpanFunction.SPM}()`]: 17.8,
+ [`avg(${SpanMetricsField.SPAN_SELF_TIME})`]: 127.1,
+ [`sum(${SpanMetricsField.SPAN_SELF_TIME})`]: 1172319,
+ [`${SpanFunction.TIME_SPENT_PERCENTAGE}()`]: 0.002,
};
it('renders basic metrics', function () {
diff --git a/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.tsx b/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.tsx
index 117184b01be5b2..3d3a9e03cd3084 100644
--- a/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.tsx
+++ b/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.tsx
@@ -4,48 +4,48 @@ import {CountCell} from 'sentry/views/starfish/components/tableCells/countCell';
import {DurationCell} from 'sentry/views/starfish/components/tableCells/durationCell';
import {ThroughputCell} from 'sentry/views/starfish/components/tableCells/throughputCell';
import {TimeSpentCell} from 'sentry/views/starfish/components/tableCells/timeSpentCell';
-import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types';
+import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types';
import {DataTitles, getThroughputTitle} from 'sentry/views/starfish/views/spans/types';
import {Block, BlockContainer} from 'sentry/views/starfish/views/spanSummaryPage/block';
interface Props {
spanMetrics: {
- [SpanMetricsFields.SPAN_OP]?: string;
- [SpanMetricsFields.SPAN_DESCRIPTION]?: string;
- [SpanMetricsFields.SPAN_ACTION]?: string;
- [SpanMetricsFields.SPAN_DOMAIN]?: string;
- [SpanMetricsFields.SPAN_GROUP]?: string;
+ [SpanMetricsField.SPAN_OP]?: string;
+ [SpanMetricsField.SPAN_DESCRIPTION]?: string;
+ [SpanMetricsField.SPAN_ACTION]?: string;
+ [SpanMetricsField.SPAN_DOMAIN]?: string;
+ [SpanMetricsField.SPAN_GROUP]?: string;
};
}
export function SpanMetricsRibbon({spanMetrics}: Props) {
- const op = spanMetrics?.[SpanMetricsFields.SPAN_OP] ?? '';
+ const op = spanMetrics?.[SpanMetricsField.SPAN_OP] ?? '';
return (
{op.startsWith('http') && (
-
+
)}
diff --git a/static/app/views/starfish/views/spanSummaryPage/spanSummaryView.tsx b/static/app/views/starfish/views/spanSummaryPage/spanSummaryView.tsx
index 5974b729d1a1a7..10f9ecbc558109 100644
--- a/static/app/views/starfish/views/spanSummaryPage/spanSummaryView.tsx
+++ b/static/app/views/starfish/views/spanSummaryPage/spanSummaryView.tsx
@@ -16,7 +16,7 @@ import {
useSpanMetrics,
} from 'sentry/views/starfish/queries/useSpanMetrics';
import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useSpanMetricsSeries';
-import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types';
+import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types';
import {
DataTitles,
getThroughputChartTitle,
@@ -51,43 +51,43 @@ export function SpanSummaryView({groupId}: Props) {
groupId,
queryFilter,
[
- SpanMetricsFields.SPAN_OP,
- SpanMetricsFields.SPAN_DESCRIPTION,
- SpanMetricsFields.SPAN_ACTION,
- SpanMetricsFields.SPAN_DOMAIN,
+ SpanMetricsField.SPAN_OP,
+ SpanMetricsField.SPAN_DESCRIPTION,
+ SpanMetricsField.SPAN_ACTION,
+ SpanMetricsField.SPAN_DOMAIN,
'count()',
- `${StarfishFunctions.SPM}()`,
- `sum(${SpanMetricsFields.SPAN_SELF_TIME})`,
- `avg(${SpanMetricsFields.SPAN_SELF_TIME})`,
- `${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`,
- `${StarfishFunctions.HTTP_ERROR_COUNT}()`,
+ `${SpanFunction.SPM}()`,
+ `sum(${SpanMetricsField.SPAN_SELF_TIME})`,
+ `avg(${SpanMetricsField.SPAN_SELF_TIME})`,
+ `${SpanFunction.TIME_SPENT_PERCENTAGE}()`,
+ `${SpanFunction.HTTP_ERROR_COUNT}()`,
],
'api.starfish.span-summary-page-metrics'
);
const span = {
...spanMetrics,
- [SpanMetricsFields.SPAN_GROUP]: groupId,
+ [SpanMetricsField.SPAN_GROUP]: groupId,
} as {
- [SpanMetricsFields.SPAN_OP]: string;
- [SpanMetricsFields.SPAN_DESCRIPTION]: string;
- [SpanMetricsFields.SPAN_ACTION]: string;
- [SpanMetricsFields.SPAN_DOMAIN]: string;
- [SpanMetricsFields.SPAN_GROUP]: string;
+ [SpanMetricsField.SPAN_OP]: string;
+ [SpanMetricsField.SPAN_DESCRIPTION]: string;
+ [SpanMetricsField.SPAN_ACTION]: string;
+ [SpanMetricsField.SPAN_DOMAIN]: string;
+ [SpanMetricsField.SPAN_GROUP]: string;
};
const {isLoading: areSpanMetricsSeriesLoading, data: spanMetricsSeriesData} =
useSpanMetricsSeries(
groupId,
queryFilter,
- [`avg(${SpanMetricsFields.SPAN_SELF_TIME})`, 'spm()', 'http_error_count()'],
+ [`avg(${SpanMetricsField.SPAN_SELF_TIME})`, 'spm()', 'http_error_count()'],
'api.starfish.span-summary-page-metrics-chart'
);
useSynchronizeCharts([!areSpanMetricsSeriesLoading]);
const spanMetricsThroughputSeries = {
- seriesName: span?.[SpanMetricsFields.SPAN_OP]?.startsWith('db')
+ seriesName: span?.[SpanMetricsField.SPAN_OP]?.startsWith('db')
? 'Queries'
: 'Requests',
data: spanMetricsSeriesData?.['spm()'].data,
@@ -103,14 +103,13 @@ export function SpanSummaryView({groupId}: Props) {
- {span?.[SpanMetricsFields.SPAN_DESCRIPTION] && (
+ {span?.[SpanMetricsField.SPAN_DESCRIPTION] && (
@@ -118,7 +117,7 @@ export function SpanSummaryView({groupId}: Props) {
-
+
- {span?.[SpanMetricsFields.SPAN_OP]?.startsWith('http') && (
+ {span?.[SpanMetricsField.SPAN_OP]?.startsWith('http') && (
;
endpoint?: string;
endpointMethod?: string;
@@ -63,15 +66,22 @@ export function SpanTransactionsTable({span, endpoint, endpointMethod, sort}: Pr
const organization = useOrganization();
const router = useRouter();
+ const cursor = decodeScalar(location.query?.[QueryParameterNames.ENDPOINTS_CURSOR]);
+
const {
data: spanTransactionMetrics = [],
meta,
isLoading,
pageLinks,
- } = useSpanTransactionMetrics(span[SpanMetricsFields.SPAN_GROUP], {
- transactions: endpoint ? [endpoint] : undefined,
- sorts: [sort],
- });
+ } = useSpanTransactionMetrics(
+ span[SpanMetricsField.SPAN_GROUP],
+ {
+ transactions: endpoint ? [endpoint] : undefined,
+ sorts: [sort],
+ },
+ undefined,
+ cursor
+ );
const spanTransactionsWithMetrics = spanTransactionMetrics.map(row => {
return {
@@ -89,7 +99,7 @@ export function SpanTransactionsTable({span, endpoint, endpointMethod, sort}: Pr
const pathname = `${routingContext.baseURL}/${
extractRoute(location) ?? 'spans'
- }/span/${encodeURIComponent(span[SpanMetricsFields.SPAN_GROUP])}`;
+ }/span/${encodeURIComponent(span[SpanMetricsField.SPAN_GROUP])}`;
const query = {
...location.query,
endpoint,
@@ -127,6 +137,13 @@ export function SpanTransactionsTable({span, endpoint, endpointMethod, sort}: Pr
return rendered;
};
+ const handleCursor: CursorHandler = (newCursor, pathname, query) => {
+ browserHistory.push({
+ pathname,
+ query: {...query, [QueryParameterNames.ENDPOINTS_CURSOR]: newCursor},
+ });
+ };
+
return (
)}
-
+
);
@@ -165,7 +182,7 @@ export function SpanTransactionsTable({span, endpoint, endpointMethod, sort}: Pr
const getColumnOrder = (
span: Pick<
SpanIndexedFieldTypes,
- SpanIndexedFields.SPAN_GROUP | SpanIndexedFields.SPAN_OP
+ SpanIndexedField.SPAN_GROUP | SpanIndexedField.SPAN_OP
>
): TableColumnHeader[] => [
{
@@ -175,11 +192,11 @@ const getColumnOrder = (
},
{
key: 'spm()',
- name: getThroughputTitle(span[SpanIndexedFields.SPAN_OP]),
+ name: getThroughputTitle(span[SpanIndexedField.SPAN_OP]),
width: COL_WIDTH_UNDEFINED,
},
{
- key: `avg(${SpanMetricsFields.SPAN_SELF_TIME})`,
+ key: `avg(${SpanMetricsField.SPAN_SELF_TIME})`,
name: DataTitles.avg,
width: COL_WIDTH_UNDEFINED,
},
diff --git a/static/app/views/starfish/views/spans/index.tsx b/static/app/views/starfish/views/spans/index.tsx
index 181f5b8abcbaa4..5d7f752e753aea 100644
--- a/static/app/views/starfish/views/spans/index.tsx
+++ b/static/app/views/starfish/views/spans/index.tsx
@@ -12,13 +12,13 @@ import {useLocation} from 'sentry/utils/useLocation';
import StarfishDatePicker from 'sentry/views/starfish/components/datePicker';
import {StarfishPageFiltersContainer} from 'sentry/views/starfish/components/starfishPageFiltersContainer';
import {StarfishProjectSelector} from 'sentry/views/starfish/components/starfishProjectSelector';
-import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types';
+import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types';
import {ROUTE_NAMES} from 'sentry/views/starfish/utils/routeNames';
import {RoutingContextProvider} from 'sentry/views/starfish/utils/routingContext';
import SpansView from './spansView';
-const {SPAN_MODULE} = SpanMetricsFields;
+const {SPAN_MODULE} = SpanMetricsField;
type Query = {
'span.category'?: string;
diff --git a/static/app/views/starfish/views/spans/selectors/actionSelector.tsx b/static/app/views/starfish/views/spans/selectors/actionSelector.tsx
index c19d5c9cad2adc..fc014787db6232 100644
--- a/static/app/views/starfish/views/spans/selectors/actionSelector.tsx
+++ b/static/app/views/starfish/views/spans/selectors/actionSelector.tsx
@@ -8,7 +8,7 @@ import {t} from 'sentry/locale';
import EventView from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {useLocation} from 'sentry/utils/useLocation';
-import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types';
+import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types';
import {buildEventViewQuery} from 'sentry/views/starfish/utils/buildEventViewQuery';
import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
import {
@@ -16,7 +16,7 @@ import {
EmptyContainer,
} from 'sentry/views/starfish/views/spans/selectors/emptyOption';
-const {SPAN_ACTION} = SpanMetricsFields;
+const {SPAN_ACTION} = SpanMetricsField;
type Props = {
moduleName?: ModuleName;
diff --git a/static/app/views/starfish/views/spans/selectors/domainSelector.tsx b/static/app/views/starfish/views/spans/selectors/domainSelector.tsx
index 252fc583a8810d..073e03d1e4ebca 100644
--- a/static/app/views/starfish/views/spans/selectors/domainSelector.tsx
+++ b/static/app/views/starfish/views/spans/selectors/domainSelector.tsx
@@ -9,7 +9,7 @@ import {t} from 'sentry/locale';
import EventView from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {useLocation} from 'sentry/utils/useLocation';
-import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types';
+import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types';
import {buildEventViewQuery} from 'sentry/views/starfish/utils/buildEventViewQuery';
import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
import {
@@ -17,7 +17,7 @@ import {
EmptyContainer,
} from 'sentry/views/starfish/views/spans/selectors/emptyOption';
-const {SPAN_DOMAIN} = SpanMetricsFields;
+const {SPAN_DOMAIN} = SpanMetricsField;
type Props = {
moduleName?: ModuleName;
diff --git a/static/app/views/starfish/views/spans/selectors/spanOperationSelector.tsx b/static/app/views/starfish/views/spans/selectors/spanOperationSelector.tsx
index 8c902b3aa6c3c0..8ffc1716478b7c 100644
--- a/static/app/views/starfish/views/spans/selectors/spanOperationSelector.tsx
+++ b/static/app/views/starfish/views/spans/selectors/spanOperationSelector.tsx
@@ -7,7 +7,7 @@ import {t} from 'sentry/locale';
import EventView from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {useLocation} from 'sentry/utils/useLocation';
-import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types';
+import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types';
import {buildEventViewQuery} from 'sentry/views/starfish/utils/buildEventViewQuery';
import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
import {
@@ -15,7 +15,7 @@ import {
EMPTY_OPTION_VALUE,
} from 'sentry/views/starfish/views/spans/selectors/emptyOption';
-const {SPAN_OP} = SpanMetricsFields;
+const {SPAN_OP} = SpanMetricsField;
type Props = {
value: string;
diff --git a/static/app/views/starfish/views/spans/spanTimeCharts.tsx b/static/app/views/starfish/views/spans/spanTimeCharts.tsx
index cd7ec1600b4dfc..152e946df1540c 100644
--- a/static/app/views/starfish/views/spans/spanTimeCharts.tsx
+++ b/static/app/views/starfish/views/spans/spanTimeCharts.tsx
@@ -12,7 +12,7 @@ import usePageFilters from 'sentry/utils/usePageFilters';
import {AVG_COLOR, ERRORS_COLOR, THROUGHPUT_COLOR} from 'sentry/views/starfish/colours';
import Chart, {useSynchronizeCharts} from 'sentry/views/starfish/components/chart';
import ChartPanel from 'sentry/views/starfish/components/chartPanel';
-import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types';
+import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types';
import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/starfish/utils/constants';
import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
import {useErrorRateQuery as useErrorCountQuery} from 'sentry/views/starfish/views/spans/queries';
@@ -24,7 +24,7 @@ import {
import {ModuleFilters} from 'sentry/views/starfish/views/spans/useModuleFilters';
import {NULL_SPAN_CATEGORY} from 'sentry/views/starfish/views/webServiceView/spanGroupBreakdownContainer';
-const {SPAN_SELF_TIME, SPAN_MODULE, SPAN_DESCRIPTION} = SpanMetricsFields;
+const {SPAN_SELF_TIME, SPAN_MODULE, SPAN_DESCRIPTION} = SpanMetricsField;
const CHART_HEIGHT = 140;
diff --git a/static/app/views/starfish/views/spans/spansTable.tsx b/static/app/views/starfish/views/spans/spansTable.tsx
index 91fc5be2c4c4fb..69fe01d083d332 100644
--- a/static/app/views/starfish/views/spans/spansTable.tsx
+++ b/static/app/views/starfish/views/spans/spansTable.tsx
@@ -18,7 +18,7 @@ import useOrganization from 'sentry/utils/useOrganization';
import {renderHeadCell} from 'sentry/views/starfish/components/tableCells/renderHeadCell';
import {SpanDescriptionCell} from 'sentry/views/starfish/components/tableCells/spanDescriptionCell';
import {useSpanList} from 'sentry/views/starfish/queries/useSpanList';
-import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types';
+import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types';
import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
import {DataTitles, getThroughputTitle} from 'sentry/views/starfish/views/spans/types';
import type {ValidSort} from 'sentry/views/starfish/views/spans/useModuleSort';
@@ -48,7 +48,7 @@ type Props = {
};
const {SPAN_SELF_TIME, SPAN_DESCRIPTION, SPAN_DOMAIN, SPAN_GROUP, SPAN_OP, PROJECT_ID} =
- SpanMetricsFields;
+ SpanMetricsField;
export default function SpansTable({
moduleName,
@@ -62,7 +62,7 @@ export default function SpansTable({
const location = useLocation();
const organization = useOrganization();
- const spansCursor = decodeScalar(location.query?.[QueryParameterNames.CURSOR]);
+ const cursor = decodeScalar(location.query?.[QueryParameterNames.SPANS_CURSOR]);
const {isLoading, data, meta, pageLinks} = useSpanList(
moduleName ?? ModuleName.ALL,
@@ -72,13 +72,13 @@ export default function SpansTable({
[sort],
limit,
'api.starfish.use-span-list',
- spansCursor
+ cursor
);
- const handleCursor: CursorHandler = (cursor, pathname, query) => {
+ const handleCursor: CursorHandler = (newCursor, pathname, query) => {
browserHistory.push({
pathname,
- query: {...query, [QueryParameterNames.CURSOR]: cursor},
+ query: {...query, [QueryParameterNames.SPANS_CURSOR]: newCursor},
});
};
diff --git a/static/app/views/starfish/views/spans/spansView.tsx b/static/app/views/starfish/views/spans/spansView.tsx
index 7b32436d8dc07d..0c6e443ebfec41 100644
--- a/static/app/views/starfish/views/spans/spansView.tsx
+++ b/static/app/views/starfish/views/spans/spansView.tsx
@@ -2,7 +2,7 @@ import {Fragment} from 'react';
import styled from '@emotion/styled';
import {space} from 'sentry/styles/space';
-import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types';
+import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types';
import {ActionSelector} from 'sentry/views/starfish/views/spans/selectors/actionSelector';
import {DomainSelector} from 'sentry/views/starfish/views/spans/selectors/domainSelector';
import {SpanOperationSelector} from 'sentry/views/starfish/views/spans/selectors/spanOperationSelector';
@@ -12,7 +12,7 @@ import {useModuleSort} from 'sentry/views/starfish/views/spans/useModuleSort';
import SpansTable from './spansTable';
-const {SPAN_ACTION, SPAN_DOMAIN, SPAN_OP} = SpanMetricsFields;
+const {SPAN_ACTION, SPAN_DOMAIN, SPAN_OP} = SpanMetricsField;
const LIMIT: number = 25;
diff --git a/static/app/views/starfish/views/spans/useModuleFilters.ts b/static/app/views/starfish/views/spans/useModuleFilters.ts
index fb7ae045b0d756..d267476faa2883 100644
--- a/static/app/views/starfish/views/spans/useModuleFilters.ts
+++ b/static/app/views/starfish/views/spans/useModuleFilters.ts
@@ -1,22 +1,22 @@
import pick from 'lodash/pick';
import {useLocation} from 'sentry/utils/useLocation';
-import {SpanMetricsFields} from 'sentry/views/starfish/types';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
export type ModuleFilters = {
- [SpanMetricsFields.SPAN_ACTION]?: string;
- [SpanMetricsFields.SPAN_DOMAIN]?: string;
- [SpanMetricsFields.SPAN_GROUP]?: string;
- [SpanMetricsFields.SPAN_OP]?: string;
+ [SpanMetricsField.SPAN_ACTION]?: string;
+ [SpanMetricsField.SPAN_DOMAIN]?: string;
+ [SpanMetricsField.SPAN_GROUP]?: string;
+ [SpanMetricsField.SPAN_OP]?: string;
};
export const useModuleFilters = () => {
const location = useLocation();
return pick(location.query, [
- SpanMetricsFields.SPAN_ACTION,
- SpanMetricsFields.SPAN_DOMAIN,
- SpanMetricsFields.SPAN_OP,
- SpanMetricsFields.SPAN_GROUP,
+ SpanMetricsField.SPAN_ACTION,
+ SpanMetricsField.SPAN_DOMAIN,
+ SpanMetricsField.SPAN_OP,
+ SpanMetricsField.SPAN_GROUP,
]);
};
diff --git a/static/app/views/starfish/views/spans/useModuleSort.ts b/static/app/views/starfish/views/spans/useModuleSort.ts
index 2bf5a0595ab9c7..7ed8ae1431293f 100644
--- a/static/app/views/starfish/views/spans/useModuleSort.ts
+++ b/static/app/views/starfish/views/spans/useModuleSort.ts
@@ -1,19 +1,19 @@
import {fromSorts} from 'sentry/utils/discover/eventView';
import type {Sort} from 'sentry/utils/discover/fields';
import {useLocation} from 'sentry/utils/useLocation';
-import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types';
+import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types';
import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
type Query = {
- [QueryParameterNames.SORT]: string;
+ [QueryParameterNames.SPANS_SORT]: string;
};
const SORTABLE_FIELDS = [
- `avg(${SpanMetricsFields.SPAN_SELF_TIME})`,
- `${StarfishFunctions.HTTP_ERROR_COUNT}()`,
- `${StarfishFunctions.SPM}()`,
- `${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`,
- `${StarfishFunctions.TIME_SPENT_PERCENTAGE}(local)`,
+ `avg(${SpanMetricsField.SPAN_SELF_TIME})`,
+ `${SpanFunction.HTTP_ERROR_COUNT}()`,
+ `${SpanFunction.SPM}()`,
+ `${SpanFunction.TIME_SPENT_PERCENTAGE}()`,
+ `${SpanFunction.TIME_SPENT_PERCENTAGE}(local)`,
] as const;
export type ValidSort = Sort & {
@@ -28,14 +28,14 @@ export function useModuleSort(fallback: Sort = DEFAULT_SORT) {
const location = useLocation();
return (
- fromSorts(location.query[QueryParameterNames.SORT]).filter(isAValidSort)[0] ??
+ fromSorts(location.query[QueryParameterNames.SPANS_SORT]).filter(isAValidSort)[0] ??
fallback
);
}
const DEFAULT_SORT: Sort = {
kind: 'desc',
- field: `${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`,
+ field: `${SpanFunction.TIME_SPENT_PERCENTAGE}()`,
};
function isAValidSort(sort: Sort): sort is ValidSort {
diff --git a/static/app/views/starfish/views/webServiceView/spanGroupBreakdown.tsx b/static/app/views/starfish/views/webServiceView/spanGroupBreakdown.tsx
index 6ad043c907b4cd..d87e0d1fc0d224 100644
--- a/static/app/views/starfish/views/webServiceView/spanGroupBreakdown.tsx
+++ b/static/app/views/starfish/views/webServiceView/spanGroupBreakdown.tsx
@@ -13,14 +13,14 @@ import {tooltipFormatterUsingAggregateOutputType} from 'sentry/utils/discover/ch
import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
import useOrganization from 'sentry/utils/useOrganization';
import Chart from 'sentry/views/starfish/components/chart';
-import {SpanMetricsFields} from 'sentry/views/starfish/types';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
import {useRoutingContext} from 'sentry/views/starfish/utils/routingContext';
import {
DataDisplayType,
DataRow,
} from 'sentry/views/starfish/views/webServiceView/spanGroupBreakdownContainer';
-const {SPAN_MODULE} = SpanMetricsFields;
+const {SPAN_MODULE} = SpanMetricsField;
type Props = {
colorPalette: string[];
diff --git a/static/app/views/starfish/views/webServiceView/spanGroupBreakdownContainer.tsx b/static/app/views/starfish/views/webServiceView/spanGroupBreakdownContainer.tsx
index 9ce90e08be1661..ee25739ab4103b 100644
--- a/static/app/views/starfish/views/webServiceView/spanGroupBreakdownContainer.tsx
+++ b/static/app/views/starfish/views/webServiceView/spanGroupBreakdownContainer.tsx
@@ -16,12 +16,12 @@ import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';
-import {SpanMetricsFields} from 'sentry/views/starfish/types';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/starfish/utils/constants';
import {useEventsStatsQuery} from 'sentry/views/starfish/utils/useEventsStatsQuery';
import {SpanGroupBreakdown} from 'sentry/views/starfish/views/webServiceView/spanGroupBreakdown';
-const {SPAN_SELF_TIME} = SpanMetricsFields;
+const {SPAN_SELF_TIME} = SpanMetricsField;
const OTHER_SPAN_GROUP_MODULE = 'Other';
export const NULL_SPAN_CATEGORY = t('custom');
diff --git a/tests/sentry/api/endpoints/test_organization_details.py b/tests/sentry/api/endpoints/test_organization_details.py
index 81489c4e99a646..e75263a1bd3c59 100644
--- a/tests/sentry/api/endpoints/test_organization_details.py
+++ b/tests/sentry/api/endpoints/test_organization_details.py
@@ -38,6 +38,7 @@
from sentry.signals import project_created
from sentry.silo import SiloMode, unguarded_write
from sentry.testutils.cases import APITestCase, TwoFactorAPITestCase
+from sentry.testutils.helpers import override_options
from sentry.testutils.outbox import outbox_runner
from sentry.testutils.silo import assume_test_silo_mode, region_silo_test
from sentry.utils import json
@@ -352,7 +353,7 @@ def test_invalid_slugs(self):
self.get_error_response(self.organization.slug, slug="canada-", status_code=400)
self.get_error_response(self.organization.slug, slug="-canada", status_code=400)
self.get_error_response(self.organization.slug, slug="----", status_code=400)
- with self.feature("app:enterprise-prevent-numeric-slugs"):
+ with override_options({"api.prevent-numeric-slugs": True}):
self.get_error_response(self.organization.slug, slug="1234", status_code=400)
def test_upload_avatar(self):
diff --git a/tests/sentry/api/endpoints/test_organization_events_trends_v2.py b/tests/sentry/api/endpoints/test_organization_events_trends_v2.py
index 29fc61ef68f260..70e5fc24b9c1d4 100644
--- a/tests/sentry/api/endpoints/test_organization_events_trends_v2.py
+++ b/tests/sentry/api/endpoints/test_organization_events_trends_v2.py
@@ -6,7 +6,7 @@
from django.urls import reverse
from freezegun import freeze_time
-from sentry.issues.grouptype import PerformanceP95TransactionDurationRegressionGroupType
+from sentry.issues.grouptype import PerformanceDurationRegressionGroupType
from sentry.snuba.metrics.naming_layer import TransactionMRI
from sentry.testutils.cases import MetricsAPIBaseTestCase
from sentry.testutils.helpers.datetime import iso_format
@@ -535,7 +535,7 @@ def test_issue_creation_simple(self, mock_get_trends, mock_produce_occurrence_to
},
{"name": "Transaction", "value": "foo", "important": True},
],
- "type": PerformanceP95TransactionDurationRegressionGroupType.type_id,
+ "type": PerformanceDurationRegressionGroupType.type_id,
"level": "info",
"culprit": "foo",
},
diff --git a/tests/sentry/api/endpoints/test_organization_index.py b/tests/sentry/api/endpoints/test_organization_index.py
index d846251d7cb94d..a7ff3995f08d5e 100644
--- a/tests/sentry/api/endpoints/test_organization_index.py
+++ b/tests/sentry/api/endpoints/test_organization_index.py
@@ -8,7 +8,7 @@
from sentry.models import Authenticator, Organization, OrganizationMember, OrganizationStatus
from sentry.silo import SiloMode
from sentry.testutils.cases import APITestCase, TwoFactorAPITestCase
-from sentry.testutils.helpers.features import with_feature
+from sentry.testutils.helpers.options import override_options
from sentry.testutils.hybrid_cloud import HybridCloudTestMixin
from sentry.testutils.silo import assume_test_silo_mode, region_silo_test
@@ -139,7 +139,7 @@ def test_invalid_slugs(self):
self.get_error_response(name="name", slug="canada-", status_code=400)
self.get_error_response(name="name", slug="-canada", status_code=400)
self.get_error_response(name="name", slug="----", status_code=400)
- with self.feature("app:enterprise-prevent-numeric-slugs"):
+ with override_options({"api.prevent-numeric-slugs": True}):
self.get_error_response(name="name", slug="1234", status_code=400)
def test_without_slug(self):
@@ -149,7 +149,7 @@ def test_without_slug(self):
org = Organization.objects.get(id=organization_id)
assert org.slug == "hello-world"
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_generated_slug_not_entirely_numeric(self):
response = self.get_success_response(name="1234")
diff --git a/tests/sentry/api/endpoints/test_organization_project_slugs.py b/tests/sentry/api/endpoints/test_organization_project_slugs.py
index 8a6db848d3ad11..ce4eceda7ef0eb 100644
--- a/tests/sentry/api/endpoints/test_organization_project_slugs.py
+++ b/tests/sentry/api/endpoints/test_organization_project_slugs.py
@@ -1,6 +1,6 @@
from fixtures.apidocs_test_case import APIDocsTestCase
from sentry.models.project import Project
-from sentry.testutils.helpers.features import with_feature
+from sentry.testutils.helpers.options import override_options
from sentry.testutils.silo import region_silo_test
@@ -30,7 +30,7 @@ def test_updates_project_slugs(self):
str(project_two.id): "new-two",
}
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_invalid_numeric_slug(self):
invalid_slugs = {**self.slugs, self.project_two.id: "1234"}
response = self.get_error_response(
diff --git a/tests/sentry/api/endpoints/test_organization_teams.py b/tests/sentry/api/endpoints/test_organization_teams.py
index 728adf8d85c1d0..3b83041ef4991f 100644
--- a/tests/sentry/api/endpoints/test_organization_teams.py
+++ b/tests/sentry/api/endpoints/test_organization_teams.py
@@ -6,7 +6,7 @@
from sentry.models import OrganizationMember, OrganizationMemberTeam, Team
from sentry.models.projectteam import ProjectTeam
from sentry.testutils.cases import APITestCase
-from sentry.testutils.helpers.features import with_feature
+from sentry.testutils.helpers.options import override_options
from sentry.testutils.silo import region_silo_test
from sentry.types.integrations import get_provider_string
@@ -251,14 +251,14 @@ def test_name_too_long(self):
self.organization.slug, name="x" * 65, slug="xxxxxxx", status_code=400
)
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_invalid_numeric_slug(self):
response = self.get_error_response(
self.organization.slug, name="hello word", slug="1234", status_code=400
)
assert response.data["slug"][0] == DEFAULT_SLUG_ERROR_MESSAGE
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_generated_slug_not_entirely_numeric(self):
response = self.get_success_response(self.organization.slug, name="1234", status_code=201)
team = Team.objects.get(id=response.data["id"])
diff --git a/tests/sentry/api/endpoints/test_project_details.py b/tests/sentry/api/endpoints/test_project_details.py
index 445ebb7322e2a1..71806d38af6b9d 100644
--- a/tests/sentry/api/endpoints/test_project_details.py
+++ b/tests/sentry/api/endpoints/test_project_details.py
@@ -35,6 +35,7 @@
from sentry.silo import SiloMode, unguarded_write
from sentry.testutils.cases import APITestCase
from sentry.testutils.helpers import Feature, with_feature
+from sentry.testutils.helpers.options import override_options
from sentry.testutils.outbox import outbox_runner
from sentry.testutils.silo import assume_test_silo_mode, region_silo_test
from sentry.types.integrations import ExternalProviders
@@ -549,7 +550,7 @@ def test_invalid_slug(self):
project = Project.objects.get(id=self.project.id)
assert project.slug != new_project.slug
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_invalid_numeric_slug(self):
response = self.get_error_response(
self.org_slug,
diff --git a/tests/sentry/api/endpoints/test_project_ownership.py b/tests/sentry/api/endpoints/test_project_ownership.py
index cacd5ba6dd4658..d71bcbb79fd509 100644
--- a/tests/sentry/api/endpoints/test_project_ownership.py
+++ b/tests/sentry/api/endpoints/test_project_ownership.py
@@ -164,7 +164,7 @@ def test_audit_log_entry(self):
with assume_test_silo_mode(SiloMode.CONTROL):
auditlog = AuditLogEntry.objects.filter(
organization_id=self.project.organization.id,
- event=audit_log.get_event_id("PROJECT_EDIT"),
+ event=audit_log.get_event_id("PROJECT_OWNERSHIPRULE_EDIT"),
target_object=self.project.id,
)
assert len(auditlog) == 1
@@ -178,7 +178,7 @@ def test_audit_log_ownership_change(self):
with assume_test_silo_mode(SiloMode.CONTROL):
auditlog = AuditLogEntry.objects.filter(
organization_id=self.project.organization.id,
- event=audit_log.get_event_id("PROJECT_EDIT"),
+ event=audit_log.get_event_id("PROJECT_OWNERSHIPRULE_EDIT"),
target_object=self.project.id,
)
assert len(auditlog) == 1
diff --git a/tests/sentry/api/endpoints/test_project_rule_details.py b/tests/sentry/api/endpoints/test_project_rule_details.py
index 1ea7e48d35be2c..16cbbfdde164de 100644
--- a/tests/sentry/api/endpoints/test_project_rule_details.py
+++ b/tests/sentry/api/endpoints/test_project_rule_details.py
@@ -454,6 +454,98 @@ def test_update_duplicate_rule(self):
== f"This rule is an exact duplicate of '{rule.label}' in this project and may not be created."
)
+ def test_duplicate_rule_environment(self):
+ """Test that if one rule doesn't have an environment set (i.e. 'All Environments') and we compare it to a rule
+ that does have one set, we consider this when determining if it's a duplicate"""
+ conditions = [
+ {
+ "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition",
+ }
+ ]
+ actions = [
+ {
+ "targetType": "IssueOwners",
+ "fallthroughType": "ActiveMembers",
+ "id": "sentry.mail.actions.NotifyEmailAction",
+ "targetIdentifier": "",
+ }
+ ]
+ self.create_project_rule(
+ project=self.project, action_match=actions, condition_match=conditions
+ )
+ env_rule = self.create_project_rule(
+ project=self.project, action_match=actions, condition_match=conditions
+ )
+ payload = {
+ "name": "hello world",
+ "actionMatch": "all",
+ "actions": actions,
+ "conditions": conditions,
+ }
+ resp = self.get_error_response(
+ self.organization.slug,
+ self.project.slug,
+ env_rule.id,
+ status_code=status.HTTP_400_BAD_REQUEST,
+ **payload,
+ )
+ assert (
+ resp.data["name"][0]
+ == f"This rule is an exact duplicate of '{env_rule.label}' in this project and may not be created."
+ )
+
+ # update env_rule to have an environment set - these should now be considered to be different
+ payload["environment"] = self.environment.name
+ resp = self.get_success_response(
+ self.organization.slug,
+ self.project.slug,
+ env_rule.id,
+ status_code=status.HTTP_200_OK,
+ **payload,
+ )
+
+ def test_duplicate_rule_actions(self):
+ """Test that if one rule doesn't have an action set (i.e. 'Do Nothing') and we compare it to a rule
+ that does have one set, we consider this when determining if it's a duplicate"""
+
+ # XXX(CEO): After we migrate old data so that no rules have no actions, this test won't be needed
+ conditions = [
+ {
+ "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition",
+ }
+ ]
+ actions = [
+ {
+ "targetType": "IssueOwners",
+ "fallthroughType": "ActiveMembers",
+ "id": "sentry.mail.actions.NotifyEmailAction",
+ "targetIdentifier": "",
+ }
+ ]
+ Rule.objects.create(
+ project=self.project,
+ data={"conditions": conditions, "action_match": "all"},
+ )
+ action_rule = Rule.objects.create(
+ project=self.project,
+ data={"conditions": conditions, "action_match": "all"},
+ )
+
+ payload = {
+ "name": "hello world",
+ "actionMatch": "all",
+ "actions": actions,
+ "conditions": conditions,
+ }
+
+ self.get_success_response(
+ self.organization.slug,
+ self.project.slug,
+ action_rule.id,
+ status_code=status.HTTP_200_OK,
+ **payload,
+ )
+
def test_edit_rule(self):
"""Test that you can edit an alert rule w/o it comparing it to itself as a dupe"""
conditions = [
diff --git a/tests/sentry/api/endpoints/test_project_rules_configuration.py b/tests/sentry/api/endpoints/test_project_rules_configuration.py
index 8b9a0a61b5a245..8ffdbbcc224dd1 100644
--- a/tests/sentry/api/endpoints/test_project_rules_configuration.py
+++ b/tests/sentry/api/endpoints/test_project_rules_configuration.py
@@ -169,3 +169,18 @@ def test_issue_type_and_category_filter_feature(self):
filter_ids = {f["id"] for f in response.data["filters"]}
assert IssueCategoryFilter.id in filter_ids
+
+ def test_issue_severity_filter_feature(self):
+ # Hide the issue severity filter when issue-severity-alerts is off
+ with self.feature({"projects:first-event-severity-alerting": False}):
+ response = self.get_success_response(self.organization.slug, self.project.slug)
+ assert "sentry.rules.filters.issue_severity.IssueSeverityFilter" not in [
+ filter["id"] for filter in response.data["filters"]
+ ]
+
+ # Show the issue severity filter when issue-severity-alerts is on
+ with self.feature({"projects:first-event-severity-alerting": True}):
+ response = self.get_success_response(self.organization.slug, self.project.slug)
+ assert "sentry.rules.filters.issue_severity.IssueSeverityFilter" in [
+ filter["id"] for filter in response.data["filters"]
+ ]
diff --git a/tests/sentry/api/endpoints/test_relay_globalconfig_v4.py b/tests/sentry/api/endpoints/test_relay_globalconfig_v4.py
index 9ba2ce97f7ee12..3e5668607596e7 100644
--- a/tests/sentry/api/endpoints/test_relay_globalconfig_v4.py
+++ b/tests/sentry/api/endpoints/test_relay_globalconfig_v4.py
@@ -1,6 +1,9 @@
+from unittest.mock import patch
+
import pytest
from django.urls import reverse
+from sentry.relay.globalconfig import get_global_config
from sentry.testutils.pytest.fixtures import django_db_all
from sentry.utils import json
@@ -26,6 +29,41 @@ def inner(version, global_):
return inner
+def test_global_config():
+ assert get_global_config() == {
+ "measurements": {
+ "builtinMeasurements": [
+ {"name": "app_start_cold", "unit": "millisecond"},
+ {"name": "app_start_warm", "unit": "millisecond"},
+ {"name": "cls", "unit": "none"},
+ {"name": "fcp", "unit": "millisecond"},
+ {"name": "fid", "unit": "millisecond"},
+ {"name": "fp", "unit": "millisecond"},
+ {"name": "frames_frozen_rate", "unit": "ratio"},
+ {"name": "frames_frozen", "unit": "none"},
+ {"name": "frames_slow_rate", "unit": "ratio"},
+ {"name": "frames_slow", "unit": "none"},
+ {"name": "frames_total", "unit": "none"},
+ {"name": "inp", "unit": "millisecond"},
+ {"name": "lcp", "unit": "millisecond"},
+ {"name": "stall_count", "unit": "none"},
+ {"name": "stall_longest_time", "unit": "millisecond"},
+ {"name": "stall_percentage", "unit": "ratio"},
+ {"name": "stall_total_time", "unit": "millisecond"},
+ {"name": "ttfb.requesttime", "unit": "millisecond"},
+ {"name": "ttfb", "unit": "millisecond"},
+ {"name": "time_to_full_display", "unit": "millisecond"},
+ {"name": "time_to_initial_display", "unit": "millisecond"},
+ ],
+ "maxCustomMeasurements": 10,
+ }
+ }
+
+
+@patch(
+ "sentry.api.endpoints.relay.project_configs.get_global_config",
+ lambda *args, **kargs: {"global_mock_config": True},
+)
@pytest.mark.parametrize(
("version, request_global_config, expect_global_config"),
[
@@ -37,11 +75,14 @@ def inner(version, global_):
)
@django_db_all
def test_return_global_config_on_right_version(
- call_endpoint, version, request_global_config, expect_global_config
+ call_endpoint,
+ version,
+ request_global_config,
+ expect_global_config,
):
result, status_code = call_endpoint(version, request_global_config)
assert status_code < 400
if not expect_global_config:
assert "global" not in result
else:
- assert result.get("global") == {}
+ assert result.get("global") == {"global_mock_config": True}
diff --git a/tests/sentry/api/endpoints/test_relay_projectconfigs_v4.py b/tests/sentry/api/endpoints/test_relay_projectconfigs_v4.py
index 838fe825d53484..873b3c504a460a 100644
--- a/tests/sentry/api/endpoints/test_relay_projectconfigs_v4.py
+++ b/tests/sentry/api/endpoints/test_relay_projectconfigs_v4.py
@@ -54,6 +54,14 @@ def projectconfig_cache_get_mock_config(monkeypatch):
)
+@pytest.fixture
+def globalconfig_get_mock_config(monkeypatch):
+ monkeypatch.setattr(
+ "sentry.relay.globalconfig.get_global_config",
+ lambda *args, **kargs: {"global_mock_config": True},
+ )
+
+
@pytest.fixture
def single_mock_proj_cached(monkeypatch):
def cache_get(*args, **kwargs):
@@ -82,7 +90,10 @@ def project_config_get_mock(monkeypatch):
@django_db_all
def test_return_full_config_if_in_cache(
- call_endpoint, default_projectkey, projectconfig_cache_get_mock_config
+ call_endpoint,
+ default_projectkey,
+ projectconfig_cache_get_mock_config,
+ globalconfig_get_mock_config,
):
result, status_code = call_endpoint(full_config=True)
assert status_code == 200
@@ -92,22 +103,28 @@ def test_return_full_config_if_in_cache(
}
+@patch(
+ "sentry.api.endpoints.relay.project_configs.get_global_config",
+ lambda *args, **kargs: {"global_mock_config": True},
+)
@django_db_all
def test_return_project_and_global_config(
- call_endpoint, default_projectkey, projectconfig_cache_get_mock_config
+ call_endpoint,
+ default_projectkey,
+ projectconfig_cache_get_mock_config,
):
result, status_code = call_endpoint(full_config=True, global_=True)
assert status_code == 200
assert result == {
"configs": {default_projectkey.public_key: {"is_mock_config": True}},
"pending": [],
- "global": {},
+ "global": {"global_mock_config": True},
}
@django_db_all
def test_proj_in_cache_and_another_pending(
- call_endpoint, default_projectkey, single_mock_proj_cached
+ call_endpoint, default_projectkey, single_mock_proj_cached, globalconfig_get_mock_config
):
result, status_code = call_endpoint(
full_config=True, public_keys=["must_exist", default_projectkey.public_key]
@@ -122,9 +139,7 @@ def test_proj_in_cache_and_another_pending(
@patch("sentry.tasks.relay.build_project_config.delay")
@django_db_all
def test_enqueue_task_if_config_not_cached_not_queued(
- schedule_mock,
- call_endpoint,
- default_projectkey,
+ schedule_mock, call_endpoint, default_projectkey, globalconfig_get_mock_config
):
result, status_code = call_endpoint(full_config=True)
assert status_code == 200
@@ -139,6 +154,7 @@ def test_debounce_task_if_proj_config_not_cached_already_enqueued(
call_endpoint,
default_projectkey,
projectconfig_debounced_cache,
+ globalconfig_get_mock_config,
):
result, status_code = call_endpoint(full_config=True)
assert status_code == 200
@@ -149,9 +165,7 @@ def test_debounce_task_if_proj_config_not_cached_already_enqueued(
@patch("sentry.relay.projectconfig_cache.backend.set_many")
@django_db_all
def test_task_writes_config_into_cache(
- cache_set_many_mock,
- default_projectkey,
- project_config_get_mock,
+ cache_set_many_mock, default_projectkey, project_config_get_mock, globalconfig_get_mock_config
):
build_project_config(
public_key=default_projectkey.public_key,
diff --git a/tests/sentry/api/endpoints/test_scim_team_details.py b/tests/sentry/api/endpoints/test_scim_team_details.py
index fb97ca28b8ad87..a516087e3091ce 100644
--- a/tests/sentry/api/endpoints/test_scim_team_details.py
+++ b/tests/sentry/api/endpoints/test_scim_team_details.py
@@ -5,7 +5,7 @@
from sentry.models import OrganizationMemberTeam, Team, TeamStatus
from sentry.testutils.cases import SCIMTestCase
-from sentry.testutils.helpers.features import with_feature
+from sentry.testutils.helpers.options import override_options
from sentry.testutils.silo import region_silo_test
@@ -134,7 +134,7 @@ def test_scim_team_details_patch_rename_team(self, mock_metrics):
"sentry.scim.team.update", tags={"organization": self.organization}
)
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_scim_team_details_patch_rename_team_invalid_slug(self):
self.base_data["Operations"] = [
{
diff --git a/tests/sentry/api/endpoints/test_scim_team_index.py b/tests/sentry/api/endpoints/test_scim_team_index.py
index 5359e5b70c2ef0..aa22cce4185c07 100644
--- a/tests/sentry/api/endpoints/test_scim_team_index.py
+++ b/tests/sentry/api/endpoints/test_scim_team_index.py
@@ -6,7 +6,7 @@
from sentry.models import Team
from sentry.signals import receivers_raise_on_send
from sentry.testutils.cases import SCIMTestCase
-from sentry.testutils.helpers.features import with_feature
+from sentry.testutils.helpers.options import override_options
from sentry.testutils.silo import region_silo_test
@@ -236,7 +236,7 @@ def test_scim_team_no_duplicate_names(self):
)
assert response.data["detail"] == "A team with this slug already exists."
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_scim_team_invalid_numeric_slug(self):
invalid_post_data = {**self.post_data, "displayName": "1234"}
response = self.get_error_response(
diff --git a/tests/sentry/api/endpoints/test_team_details.py b/tests/sentry/api/endpoints/test_team_details.py
index fca1b12b576eb6..444f5f9b8c77c2 100644
--- a/tests/sentry/api/endpoints/test_team_details.py
+++ b/tests/sentry/api/endpoints/test_team_details.py
@@ -5,6 +5,7 @@
from sentry.testutils.asserts import assert_org_audit_log_exists
from sentry.testutils.cases import APITestCase
from sentry.testutils.helpers import with_feature
+from sentry.testutils.helpers.options import override_options
from sentry.testutils.outbox import outbox_runner
from sentry.testutils.silo import assume_test_silo_mode, region_silo_test
@@ -84,7 +85,7 @@ def test_simple(self):
assert team.name == "hello world"
assert team.slug == "foobar"
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_invalid_numeric_slug(self):
response = self.get_error_response(self.organization.slug, self.team.slug, slug="1234")
assert response.data["slug"][0] == (
diff --git a/tests/sentry/api/endpoints/test_team_projects.py b/tests/sentry/api/endpoints/test_team_projects.py
index bd54a364ea4075..25a699e6b34c92 100644
--- a/tests/sentry/api/endpoints/test_team_projects.py
+++ b/tests/sentry/api/endpoints/test_team_projects.py
@@ -3,6 +3,7 @@
from sentry.notifications.types import FallthroughChoiceType
from sentry.testutils.cases import APITestCase
from sentry.testutils.helpers import with_feature
+from sentry.testutils.helpers.options import override_options
from sentry.testutils.silo import region_silo_test
@@ -62,7 +63,7 @@ def test_simple(self):
assert project.platform == "python"
assert project.teams.first() == self.team
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_invalid_numeric_slug(self):
response = self.get_error_response(
self.organization.slug,
@@ -74,7 +75,7 @@ def test_invalid_numeric_slug(self):
assert response.data["slug"][0] == DEFAULT_SLUG_ERROR_MESSAGE
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_generated_slug_not_entirely_numeric(self):
response = self.get_success_response(
self.organization.slug,
diff --git a/tests/sentry/api/endpoints/test_user_authenticator_details.py b/tests/sentry/api/endpoints/test_user_authenticator_details.py
index de9d39c7dac660..a87af5d7d829bf 100644
--- a/tests/sentry/api/endpoints/test_user_authenticator_details.py
+++ b/tests/sentry/api/endpoints/test_user_authenticator_details.py
@@ -351,22 +351,26 @@ def test_require_2fa__can_delete_last_auth_superuser(self):
superuser = self.create_user(email="a@example.com", is_superuser=True)
self.login_as(user=superuser, superuser=True)
- # enroll in one auth method
- interface = TotpInterface()
- interface.enroll(self.user)
- assert interface.authenticator is not None
- auth = interface.authenticator
+ new_options = settings.SENTRY_OPTIONS.copy()
+ new_options["sms.twilio-account"] = "twilio-account"
- with self.tasks():
- self.get_success_response(
- self.user.id,
- auth.id,
- method="delete",
- status_code=status.HTTP_204_NO_CONTENT,
- )
- assert_security_email_sent("mfa-removed")
+ with self.settings(SENTRY_OPTIONS=new_options):
+ # enroll in one auth method
+ interface = TotpInterface()
+ interface.enroll(self.user)
+ assert interface.authenticator is not None
+ auth = interface.authenticator
- assert not Authenticator.objects.filter(id=auth.id).exists()
+ with self.tasks():
+ self.get_success_response(
+ self.user.id,
+ auth.id,
+ method="delete",
+ status_code=status.HTTP_204_NO_CONTENT,
+ )
+ assert_security_email_sent("mfa-removed")
+
+ assert not Authenticator.objects.filter(id=auth.id).exists()
def test_require_2fa__delete_with_multiple_auth__ok(self):
self._require_2fa_for_organization()
diff --git a/tests/sentry/audit_log/test_register.py b/tests/sentry/audit_log/test_register.py
index 90ac181c534181..7e2031c3063d38 100644
--- a/tests/sentry/audit_log/test_register.py
+++ b/tests/sentry/audit_log/test_register.py
@@ -31,6 +31,7 @@ def test_get_api_names(self):
"project.accept-transfer",
"project.enable",
"project.disable",
+ "project.ownership-rule.edit",
"tagkey.remove",
"projectkey.create",
"projectkey.edit",
diff --git a/tests/sentry/backup/test_exports.py b/tests/sentry/backup/test_exports.py
new file mode 100644
index 00000000000000..ff15e340d0713e
--- /dev/null
+++ b/tests/sentry/backup/test_exports.py
@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+import tempfile
+from pathlib import Path
+
+from sentry.backup.helpers import get_exportable_final_derivations_of
+from sentry.backup.scopes import ExportScope
+from sentry.db.models.base import BaseModel
+from sentry.testutils.helpers.backups import BackupTestCase, export_to_file
+from tests.sentry.backup import run_backup_tests_only_on_single_db
+
+
+@run_backup_tests_only_on_single_db
+class ScopingTests(BackupTestCase):
+ """
+ Ensures that only models with the allowed relocation scopes are actually exported.
+ """
+
+ @staticmethod
+ def get_models_for_scope(scope: ExportScope) -> set[str]:
+ matching_models = set()
+ for model in get_exportable_final_derivations_of(BaseModel):
+ if model.__relocation_scope__ in scope.value:
+ obj_name = model._meta.object_name
+ if obj_name is not None:
+ matching_models.add("sentry." + obj_name.lower())
+ return matching_models
+
+ def test_user_export_scoping(self):
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ matching_models = self.get_models_for_scope(ExportScope.User)
+ self.create_exhaustive_instance(is_superadmin=True)
+ tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
+ data = export_to_file(tmp_path, ExportScope.User)
+
+ for entry in data:
+ model_name = entry["model"]
+ if model_name not in matching_models:
+ raise AssertionError(
+ f"Model `${model_name}` was included in export despite not being `Relocation.User`"
+ )
+
+ def test_organization_export_scoping(self):
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ matching_models = self.get_models_for_scope(ExportScope.Organization)
+ self.create_exhaustive_instance(is_superadmin=True)
+ tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
+ data = export_to_file(tmp_path, ExportScope.Organization)
+
+ for entry in data:
+ model_name = entry["model"]
+ if model_name not in matching_models:
+ raise AssertionError(
+ f"Model `${model_name}` was included in export despite not being `Relocation.User` or `Relocation.Organization`"
+ )
diff --git a/tests/sentry/backup/test_imports.py b/tests/sentry/backup/test_imports.py
new file mode 100644
index 00000000000000..aa37ccd600a327
--- /dev/null
+++ b/tests/sentry/backup/test_imports.py
@@ -0,0 +1,186 @@
+from __future__ import annotations
+
+import tempfile
+from copy import deepcopy
+from functools import cached_property
+from pathlib import Path
+
+import pytest
+from django.db import IntegrityError
+
+from sentry.backup.helpers import get_exportable_final_derivations_of
+from sentry.backup.imports import (
+ import_in_global_scope,
+ import_in_organization_scope,
+ import_in_user_scope,
+)
+from sentry.backup.scopes import RelocationScope
+from sentry.db.models.base import BaseModel
+from sentry.models.user import User
+from sentry.models.userpermission import UserPermission
+from sentry.models.userrole import UserRole, UserRoleUser
+from sentry.testutils.factories import get_fixture_path
+from sentry.testutils.helpers.backups import (
+ NOOP_PRINTER,
+ BackupTestCase,
+ clear_database_but_keep_sequences,
+)
+from sentry.utils import json
+from tests.sentry.backup import run_backup_tests_only_on_single_db
+
+
+@run_backup_tests_only_on_single_db
+class SanitizationTests(BackupTestCase):
+ """
+ Ensure that potentially damaging data is properly scrubbed at import time.
+ """
+
+ @cached_property
+ def json_of_exhaustive_user_with_maximum_privileges(self) -> json.JSONData:
+ with open(get_fixture_path("backup", "user-with-maximum-privileges.json")) as backup_file:
+ return json.load(backup_file)
+
+ @cached_property
+ def json_of_exhaustive_user_with_minimum_privileges(self) -> json.JSONData:
+ with open(get_fixture_path("backup", "user-with-minimum-privileges.json")) as backup_file:
+ return json.load(backup_file)
+
+ @staticmethod
+ def copy_user(exhaustive_user: json.JSONData, username: str) -> json.JSONData:
+ user = deepcopy(exhaustive_user)
+
+ for model in user:
+ if model["model"] == "sentry.user":
+ model["fields"]["username"] = username
+
+ return user
+
+ def generate_tmp_json_file(self, tmp_path) -> json.JSONData:
+ """
+ Generates a file filled with users with different combinations of admin privileges.
+ """
+
+ # A user with the maximal amount of "evil" settings.
+ max_user = self.copy_user(self.json_of_exhaustive_user_with_maximum_privileges, "max_user")
+
+ # A user with no "evil" settings.
+ min_user = self.copy_user(self.json_of_exhaustive_user_with_minimum_privileges, "min_user")
+
+ # A copy of the `min_user`, but with a maximal `UserPermissions` attached.
+ permission_user = self.copy_user(min_user, "permission_user") + deepcopy(
+ list(filter(lambda mod: mod["model"] == "sentry.userpermission", max_user))
+ )
+
+ # A copy of the `min_user`, but with all of the "evil" flags set to `True`.
+ superadmin_user = self.copy_user(min_user, "superadmin_user")
+ for model in superadmin_user:
+ if model["model"] == "sentry.user":
+ model["fields"]["is_staff"] = True
+ model["fields"]["is_superuser"] = True
+
+ data = max_user + min_user + permission_user + superadmin_user
+ with open(tmp_path, "w+") as tmp_file:
+ json.dump(data, tmp_file)
+
+ def test_user_sanitized_in_user_scope(self):
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
+ self.generate_tmp_json_file(tmp_path)
+ with open(tmp_path) as tmp_file:
+ import_in_user_scope(tmp_file, NOOP_PRINTER)
+
+ assert User.objects.count() == 4
+ assert User.objects.filter(is_staff=False, is_superuser=False).count() == 4
+
+ assert User.objects.filter(is_staff=True).count() == 0
+ assert User.objects.filter(is_superuser=True).count() == 0
+ assert UserPermission.objects.count() == 0
+ assert UserRole.objects.count() == 0
+ assert UserRoleUser.objects.count() == 0
+
+ def test_user_sanitized_in_organization_scope(self):
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
+ self.generate_tmp_json_file(tmp_path)
+ with open(tmp_path) as tmp_file:
+ import_in_organization_scope(tmp_file, NOOP_PRINTER)
+
+ assert User.objects.count() == 4
+ assert User.objects.filter(is_staff=False, is_superuser=False).count() == 4
+
+ assert User.objects.filter(is_staff=True).count() == 0
+ assert User.objects.filter(is_superuser=True).count() == 0
+ assert UserPermission.objects.count() == 0
+ assert UserRole.objects.count() == 0
+ assert UserRoleUser.objects.count() == 0
+
+ def test_users_sanitized_in_global_scope(self):
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
+ self.generate_tmp_json_file(tmp_path)
+ with open(tmp_path) as tmp_file:
+ import_in_global_scope(tmp_file, NOOP_PRINTER)
+
+ assert User.objects.count() == 4
+ assert User.objects.filter(is_staff=True).count() == 2
+ assert User.objects.filter(is_superuser=True).count() == 2
+ assert User.objects.filter(is_staff=False, is_superuser=False).count() == 2
+
+ # 1 from `max_user`, 1 from `permission_user`.
+ assert UserPermission.objects.count() == 2
+
+ # 1 from `max_user`.
+ assert UserRole.objects.count() == 1
+ assert UserRoleUser.objects.count() == 1
+
+ # TODO(getsentry/team-ospo#181): Should fix this behavior to handle duplicate
+ def test_bad_already_taken_username(self):
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ self.create_user("testing@example.com")
+ tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
+ with open(tmp_path, "w+") as tmp_file:
+ json.dump(self.json_of_exhaustive_user_with_minimum_privileges, tmp_file)
+
+ with open(tmp_path) as tmp_file:
+ with pytest.raises(IntegrityError):
+ import_in_user_scope(tmp_file, NOOP_PRINTER)
+
+
+@run_backup_tests_only_on_single_db
+class ScopingTests(BackupTestCase):
+ """
+ Ensures that only models with the allowed relocation scopes are actually imported.
+ """
+
+ def test_user_import_scoping(self):
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ self.create_exhaustive_instance(is_superadmin=True)
+ data = self.import_export_then_validate(self._testMethodName, reset_pks=True)
+ tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
+ with open(tmp_path, "w+") as tmp_file:
+ json.dump(data, tmp_file)
+
+ clear_database_but_keep_sequences()
+ with open(tmp_path) as tmp_file:
+ import_in_user_scope(tmp_file, NOOP_PRINTER)
+ for model in get_exportable_final_derivations_of(BaseModel):
+ if model.__relocation_scope__ != RelocationScope.User:
+ assert model.objects.count() == 0
+
+ def test_organization_import_scoping(self):
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ self.create_exhaustive_instance(is_superadmin=True)
+ data = self.import_export_then_validate(self._testMethodName, reset_pks=True)
+ tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
+ with open(tmp_path, "w+") as tmp_file:
+ json.dump(data, tmp_file)
+
+ clear_database_but_keep_sequences()
+ with open(tmp_path) as tmp_file:
+ import_in_organization_scope(tmp_file, NOOP_PRINTER)
+ for model in get_exportable_final_derivations_of(BaseModel):
+ if model.__relocation_scope__ not in {
+ RelocationScope.User,
+ RelocationScope.Organization,
+ }:
+ assert model.objects.count() == 0
diff --git a/tests/sentry/db/models/test_utils.py b/tests/sentry/db/models/test_utils.py
index 6b8d6b9f7eed84..6a9b07a85fc532 100644
--- a/tests/sentry/db/models/test_utils.py
+++ b/tests/sentry/db/models/test_utils.py
@@ -1,7 +1,7 @@
from sentry.db.models.utils import slugify_instance
from sentry.models import Organization
from sentry.testutils.cases import TestCase
-from sentry.testutils.helpers.features import with_feature
+from sentry.testutils.helpers.options import override_options
class SlugifyInstanceTest(TestCase):
@@ -27,7 +27,7 @@ def test_max_length(self):
slugify_instance(org, org.name, max_length=2)
assert org.slug == "ma"
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_appends_to_entirely_numeric(self):
org = Organization(name="1234")
slugify_instance(org, org.name)
diff --git a/tests/sentry/integrations/slack/test_disable.py b/tests/sentry/integrations/slack/test_disable.py
index 29d78835af8f2f..b2beadbe88ed58 100644
--- a/tests/sentry/integrations/slack/test_disable.py
+++ b/tests/sentry/integrations/slack/test_disable.py
@@ -15,7 +15,6 @@
from sentry.models import AuditLogEntry, Integration
from sentry.shared_integrations.exceptions import ApiError
from sentry.testutils.cases import TestCase
-from sentry.testutils.helpers import with_feature
from sentry.utils import json
control_address = "http://controlserver"
@@ -49,10 +48,9 @@ def tearDown(self):
self.resp.__exit__(None, None, None)
@responses.activate
- @with_feature("organizations:slack-fatal-disable-on-broken")
def test_fatal_and_disable_integration(self):
"""
- fatal fast shut off with disable flag on, integration should be broken and disabled
+ fatal fast shut off integration should be broken and disabled
"""
bodydict = {"ok": False, "error": "account_inactive"}
@@ -98,27 +96,6 @@ def test_email(self):
in msg.body
)
- @responses.activate
- def test_fatal_integration(self):
- """
- fatal fast shut off with disable flag off, integration should be broken but not disabled
- """
- bodydict = {"ok": False, "error": "account_inactive"}
- self.resp.add(
- method=responses.POST,
- url="https://slack.com/api/chat.postMessage",
- status=200,
- content_type="application/json",
- body=json.dumps(bodydict),
- )
- client = SlackClient(integration_id=self.integration.id)
- with pytest.raises(ApiError):
- client.post("/chat.postMessage", data=self.payload)
- buffer = IntegrationRequestBuffer(client._get_redis_key())
- assert buffer.is_integration_broken() is True
- integration = Integration.objects.get(id=self.integration.id)
- assert integration.status == ObjectStatus.ACTIVE
-
@responses.activate
def test_error_integration(self):
"""
@@ -149,10 +126,9 @@ def test_error_integration(self):
assert buffer.is_integration_broken() is False
@responses.activate
- @with_feature("organizations:slack-fatal-disable-on-broken")
def test_slow_integration_is_not_broken_or_disabled(self):
"""
- slow test with disable flag on
+ slow test
put errors and success in buffer for 10 days, assert integration is not broken or disabled
"""
bodydict = {"ok": False, "error": "The requested resource does not exist"}
@@ -178,8 +154,9 @@ def test_slow_integration_is_not_broken_or_disabled(self):
@responses.activate
def test_a_slow_integration_is_broken(self):
"""
- slow shut off with disable flag off
- put errors in buffer for 10 days, assert integration is broken but not disabled
+ slow shut off
+ put errors in buffer for 10 days, assert integration is broken and not disabled
+ since only fatal shut off should disable
"""
bodydict = {"ok": False, "error": "The requested resource does not exist"}
self.resp.add(
diff --git a/tests/sentry/monitors/endpoints/test_organization_monitor_details.py b/tests/sentry/monitors/endpoints/test_organization_monitor_details.py
index 720718bbb23b2f..8e8fe3cc5db597 100644
--- a/tests/sentry/monitors/endpoints/test_organization_monitor_details.py
+++ b/tests/sentry/monitors/endpoints/test_organization_monitor_details.py
@@ -5,7 +5,7 @@
from sentry.models import Environment, RegionScheduledDeletion, Rule, RuleActivity, RuleActivityType
from sentry.monitors.models import Monitor, MonitorEnvironment, ScheduleType
from sentry.testutils.cases import MonitorTestCase
-from sentry.testutils.helpers.features import with_feature
+from sentry.testutils.helpers.options import override_options
from sentry.testutils.silo import region_silo_test
@@ -98,7 +98,7 @@ def test_slug(self):
self.organization.slug, monitor.slug, method="PUT", status_code=400, **{"slug": None}
)
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_invalid_numeric_slug(self):
monitor = self._create_monitor()
resp = self.get_error_response(
diff --git a/tests/sentry/monitors/endpoints/test_organization_monitor_index.py b/tests/sentry/monitors/endpoints/test_organization_monitor_index.py
index d5f5836faa164e..780d24763e501b 100644
--- a/tests/sentry/monitors/endpoints/test_organization_monitor_index.py
+++ b/tests/sentry/monitors/endpoints/test_organization_monitor_index.py
@@ -11,7 +11,7 @@
from sentry.models import Rule, RuleSource
from sentry.monitors.models import Monitor, MonitorStatus, MonitorType, ScheduleType
from sentry.testutils.cases import MonitorTestCase
-from sentry.testutils.helpers.features import with_feature
+from sentry.testutils.helpers.options import override_options
from sentry.testutils.silo import region_silo_test
@@ -212,7 +212,7 @@ def test_slug(self):
assert response.data["slug"] == "my-monitor"
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_invalid_numeric_slug(self):
data = {
"project": self.project.slug,
@@ -224,7 +224,7 @@ def test_invalid_numeric_slug(self):
response = self.get_error_response(self.organization.slug, **data, status_code=400)
assert response.data["slug"][0] == DEFAULT_SLUG_ERROR_MESSAGE
- @with_feature("app:enterprise-prevent-numeric-slugs")
+ @override_options({"api.prevent-numeric-slugs": True})
def test_generated_slug_not_entirely_numeric(self):
data = {
"project": self.project.slug,
diff --git a/tests/sentry/monitors/test_tasks.py b/tests/sentry/monitors/test_tasks.py
index e3f1bc39eec6ee..5c3e2daa0d3e5a 100644
--- a/tests/sentry/monitors/test_tasks.py
+++ b/tests/sentry/monitors/test_tasks.py
@@ -174,7 +174,7 @@ def test_missing_checkin_with_margin(self):
== monitor_environment_updated.next_checkin + timedelta(minutes=5)
)
- def assert_state_does_not_change_for_state(self, state):
+ def assert_state_does_not_change_for_status(self, state):
org = self.create_organization()
project = self.create_project(organization=org)
@@ -210,13 +210,13 @@ def assert_state_does_not_change_for_state(self, state):
).exists()
def test_missing_checkin_but_disabled(self):
- self.assert_state_does_not_change_for_state(ObjectStatus.DISABLED)
+ self.assert_state_does_not_change_for_status(ObjectStatus.DISABLED)
def test_missing_checkin_but_pending_deletion(self):
- self.assert_state_does_not_change_for_state(ObjectStatus.PENDING_DELETION)
+ self.assert_state_does_not_change_for_status(ObjectStatus.PENDING_DELETION)
def test_missing_checkin_but_deletion_in_progress(self):
- self.assert_state_does_not_change_for_state(ObjectStatus.DELETION_IN_PROGRESS)
+ self.assert_state_does_not_change_for_status(ObjectStatus.DELETION_IN_PROGRESS)
def test_not_missing_checkin(self):
"""
diff --git a/tests/sentry/relay/config/test_metric_extraction.py b/tests/sentry/relay/config/test_metric_extraction.py
index 279d5bb96bcedd..9f6255aed09d08 100644
--- a/tests/sentry/relay/config/test_metric_extraction.py
+++ b/tests/sentry/relay/config/test_metric_extraction.py
@@ -1,6 +1,8 @@
from typing import Sequence
from unittest.mock import ANY
+import pytest
+
from sentry.incidents.models import AlertRule
from sentry.models import (
Dashboard,
@@ -20,13 +22,17 @@
ON_DEMAND_METRICS = "organizations:on-demand-metrics-extraction"
ON_DEMAND_METRICS_WIDGETS = "organizations:on-demand-metrics-extraction-experimental"
+ON_DEMAND_METRICS_PREFILL = "organizations:on-demand-metrics-prefill"
+ON_DEMAND_METRIC_PREFILL_ENABLE = "organizations:enable-on-demand-metrics-prefill"
-def create_alert(aggregate: str, query: str, project: Project) -> AlertRule:
+def create_alert(
+ aggregate: str, query: str, project: Project, dataset: Dataset = Dataset.PerformanceMetrics
+) -> AlertRule:
snuba_query = SnubaQuery.objects.create(
aggregate=aggregate,
query=query,
- dataset=Dataset.PerformanceMetrics.value,
+ dataset=dataset.value,
time_window=300,
resolution=60,
environment=None,
@@ -393,3 +399,78 @@ def test_get_metric_extraction_config_with_apdex(default_project):
{"key": "query_hash", "value": ANY},
],
}
+
+
+@django_db_all
+@pytest.mark.parametrize(
+ "enabled_features, number_of_metrics",
+ [
+ ([ON_DEMAND_METRICS], 1), # Only alerts.
+ ([ON_DEMAND_METRICS_WIDGETS, ON_DEMAND_METRICS_PREFILL], 0), # Nothing.
+ ([ON_DEMAND_METRICS, ON_DEMAND_METRICS_WIDGETS], 2), # Alerts and widgets.
+ ([ON_DEMAND_METRICS_PREFILL, ON_DEMAND_METRIC_PREFILL_ENABLE], 1), # Alerts.
+ ([ON_DEMAND_METRICS_PREFILL], 0), # Nothing.
+ ([ON_DEMAND_METRICS, ON_DEMAND_METRICS_PREFILL], 1), # Alerts.
+ ([ON_DEMAND_METRICS, ON_DEMAND_METRIC_PREFILL_ENABLE], 1), # Alerts.
+ ([], 0), # Nothing.
+ ],
+)
+def test_get_metrics_extraction_config_features_combinations(
+ enabled_features, number_of_metrics, default_project
+):
+ create_alert("count()", "transaction.duration:>=10", default_project)
+ create_widget(["count()"], "transaction.duration:>=20", default_project)
+
+ features = {feature: True for feature in enabled_features}
+ with Feature(features):
+ config = get_metric_extraction_config(default_project)
+ if number_of_metrics == 0:
+ assert config is None
+ else:
+ assert config is not None
+ assert len(config["metrics"]) == number_of_metrics
+
+
+@django_db_all
+def test_get_metric_extraction_config_with_transactions_dataset(default_project):
+ create_alert(
+ "count()", "transaction.duration:>=10", default_project, dataset=Dataset.PerformanceMetrics
+ )
+ create_alert(
+ "count()", "transaction.duration:>=20", default_project, dataset=Dataset.Transactions
+ )
+
+ # We test with prefilling, and we expect that both alerts are fetched since we support both datasets.
+ with Feature({ON_DEMAND_METRICS_PREFILL: True, ON_DEMAND_METRIC_PREFILL_ENABLE: True}):
+ config = get_metric_extraction_config(default_project)
+
+ assert config
+ assert len(config["metrics"]) == 2
+ assert config["metrics"][0] == {
+ "category": "transaction",
+ "condition": {"name": "event.duration", "op": "gte", "value": 10.0},
+ "field": None,
+ "mri": "c:transactions/on_demand@none",
+ "tags": [{"key": "query_hash", "value": ANY}],
+ }
+ assert config["metrics"][1] == {
+ "category": "transaction",
+ "condition": {"name": "event.duration", "op": "gte", "value": 20.0},
+ "field": None,
+ "mri": "c:transactions/on_demand@none",
+ "tags": [{"key": "query_hash", "value": ANY}],
+ }
+
+ # We test without prefilling, and we expect that only alerts for performance metrics are fetched.
+ with Feature({ON_DEMAND_METRICS: True}):
+ config = get_metric_extraction_config(default_project)
+
+ assert config
+ assert len(config["metrics"]) == 1
+ assert config["metrics"][0] == {
+ "category": "transaction",
+ "condition": {"name": "event.duration", "op": "gte", "value": 10.0},
+ "field": None,
+ "mri": "c:transactions/on_demand@none",
+ "tags": [{"key": "query_hash", "value": ANY}],
+ }
diff --git a/tests/sentry/rules/filters/test_issue_severity.py b/tests/sentry/rules/filters/test_issue_severity.py
new file mode 100644
index 00000000000000..08e377e8782c6e
--- /dev/null
+++ b/tests/sentry/rules/filters/test_issue_severity.py
@@ -0,0 +1,71 @@
+from unittest.mock import patch
+
+from sentry.rules import MatchType
+from sentry.rules.filters.issue_severity import IssueSeverityFilter
+from sentry.testutils.cases import RuleTestCase
+from sentry.testutils.helpers.features import with_feature
+
+
+class IssueSeverityFilterTest(RuleTestCase):
+ rule_cls = IssueSeverityFilter
+
+ @patch("sentry.models.Group.objects.get_from_cache")
+ @with_feature("projects:first-event-severity-alerting")
+ def test_valid_input_values(self, mock_group):
+ event = self.get_event()
+ event.group.data["metadata"] = {"severity": "0.7"}
+ mock_group.return_value = event.group
+
+ data_cases = [
+ {"match": MatchType.GREATER_OR_EQUAL, "value": 0.5},
+ {"match": MatchType.GREATER_OR_EQUAL, "value": 0.7},
+ {"match": MatchType.LESS_OR_EQUAL, "value": 0.7},
+ {"match": MatchType.LESS_OR_EQUAL, "value": 0.9},
+ {"match": MatchType.LESS_OR_EQUAL, "value": "0.9"},
+ ]
+
+ for data_case in data_cases:
+ rule = self.get_rule(data=data_case)
+ self.assertPasses(rule, event)
+ assert self.passes_activity(rule) is True
+
+ @with_feature("projects:first-event-severity-alerting")
+ def test_fail_on_no_group(self):
+ event = self.get_event()
+ event.group_id = None
+ event.groups = None
+
+ self.assertDoesNotPass(
+ self.get_rule(data={"match": MatchType.GREATER_OR_EQUAL, "value": 0.5}), event
+ )
+
+ @with_feature("projects:first-event-severity-alerting")
+ def test_fail_on_no_severity(self):
+ event = self.get_event()
+
+ assert not event.group.get_event_metadata().get("severity")
+ self.assertDoesNotPass(
+ self.get_rule(data={"match": MatchType.GREATER_OR_EQUAL, "value": 0.5}), event
+ )
+
+ @patch("sentry.models.Group.objects.get_from_cache")
+ @with_feature("projects:first-event-severity-alerting")
+ def test_failing_input_values(self, mock_group):
+ event = self.get_event()
+ event.group.data["metadata"] = {"severity": "0.7"}
+ mock_group.return_value = event.group
+
+ data_cases = [
+ {"match": MatchType.GREATER_OR_EQUAL, "value": 0.9},
+ {"match": MatchType.GREATER_OR_EQUAL, "value": "0.9"},
+ {"match": MatchType.LESS_OR_EQUAL, "value": "0.5"},
+ {"match": MatchType.LESS_OR_EQUAL, "value": 0.5},
+ {"value": 0.5},
+ {"match": MatchType.GREATER_OR_EQUAL},
+ {},
+ ]
+
+ for data_case in data_cases:
+ rule = self.get_rule(data=data_case)
+ self.assertDoesNotPass(rule, event)
+ assert self.passes_activity(rule) is False
diff --git a/tests/sentry/tasks/test_sentry_apps.py b/tests/sentry/tasks/test_sentry_apps.py
index 7cee9a87268360..90da856b0f6185 100644
--- a/tests/sentry/tasks/test_sentry_apps.py
+++ b/tests/sentry/tasks/test_sentry_apps.py
@@ -10,7 +10,7 @@
from freezegun import freeze_time
from requests.exceptions import Timeout
-from sentry import audit_log, features
+from sentry import audit_log
from sentry.api.serializers import serialize
from sentry.constants import SentryAppStatus
from sentry.integrations.notify_disable import notify_disable
@@ -775,7 +775,6 @@ def test_saves_error_for_request_timeout(self, safe_urlopen):
assert self.integration_buffer._get_all_from_buffer() == []
assert self.integration_buffer.is_integration_broken() is False
- @with_feature("organizations:disable-sentryapps-on-broken")
@patch(
"sentry.utils.sentry_apps.webhooks.safe_urlopen",
return_value=MockResponseWithHeadersInstance,
@@ -798,7 +797,6 @@ def test_saves_error_event_id_if_in_header(self, safe_urlopen):
assert first_request["error_id"] == "d5111da2c28645c5889d072017e3445d"
assert first_request["project_id"] == "1"
- @with_feature("organizations:disable-sentryapps-on-broken")
@patch("sentry.utils.sentry_apps.webhooks.safe_urlopen", side_effect=Timeout)
def test_does_not_raise_error_if_unpublished(self, safe_urlopen):
"""
@@ -820,7 +818,6 @@ def test_does_not_raise_error_if_unpublished(self, safe_urlopen):
assert self.sentry_app.events == events # check that events are the same / app is enabled
@patch("sentry.utils.sentry_apps.webhooks.safe_urlopen", side_effect=Timeout)
- @with_feature("organizations:disable-sentryapps-on-broken")
@override_settings(BROKEN_TIMEOUT_THRESHOLD=3)
def test_timeout_disable(self, safe_urlopen):
"""
@@ -833,40 +830,18 @@ def test_timeout_disable(self, safe_urlopen):
send_webhooks(
installation=self.install, event="issue.assigned", data=data, actor=self.user
)
- assert features.has("organizations:disable-sentryapps-on-broken", self.organization)
assert safe_urlopen.called
assert [len(item) == 0 for item in self.integration_buffer._get_broken_range_from_buffer()]
assert len(self.integration_buffer._get_all_from_buffer()) == 0
self.sentry_app.refresh_from_db() # reload to get updated events
assert len(self.sentry_app.events) == 0 # check that events are empty / app is disabled
- @patch("sentry.utils.sentry_apps.webhooks.safe_urlopen", side_effect=Timeout)
- @override_settings(BROKEN_TIMEOUT_THRESHOLD=3)
- def test_timeout_would_disable(self, safe_urlopen):
- """
- Tests that the integration would be disabled if the feature flag is enabled but is not
- """
- self.sentry_app.update(status=SentryAppStatus.INTERNAL)
- events = self.sentry_app.events # save events to check later
- data = {"issue": serialize(self.issue)}
- # we don't raise errors for unpublished and internal apps
- for i in range(3):
- send_webhooks(
- installation=self.install, event="issue.assigned", data=data, actor=self.user
- )
- assert safe_urlopen.called
- assert (self.integration_buffer._get_all_from_buffer()[0]["timeout_count"]) == "3"
- assert self.integration_buffer.is_integration_broken() is True
- self.sentry_app.refresh_from_db() # reload to get updated events
- assert self.sentry_app.events == events # check that events are the same / app is enabled
-
@patch(
"sentry.utils.sentry_apps.webhooks.safe_urlopen", return_value=MockFailureResponseInstance
)
- @with_feature("organizations:disable-sentryapps-on-broken")
def test_slow_should_disable(self, safe_urlopen):
"""
- Tests that the integration is broken after 7 days of errors and disabled since flag is on
+ Tests that the integration is broken after 7 days of errors and disabled
Slow shut off
"""
self.sentry_app.update(status=SentryAppStatus.INTERNAL)
@@ -888,31 +863,6 @@ def test_slow_should_disable(self, safe_urlopen):
organization_id=self.organization.id,
).exists()
- @patch(
- "sentry.utils.sentry_apps.webhooks.safe_urlopen", return_value=MockFailureResponseInstance
- )
- @freeze_time("2022-01-01 03:30:00")
- def test_slow_broken_not_disable(self, safe_urlopen):
- """
- Tests that the integration is broken after 10 days of errors but still enabled since flag is off
- Slow shut off
- """
- self.sentry_app.update(status=SentryAppStatus.INTERNAL)
- events = self.sentry_app.events # save events to check later
- data = {"issue": serialize(self.issue)}
- now = datetime.now()
- for i in reversed(range(0, 10)):
- with freeze_time(now - timedelta(days=i)):
- send_webhooks(
- installation=self.install, event="issue.assigned", data=data, actor=self.user
- )
-
- assert safe_urlopen.called
- assert len(self.integration_buffer._get_all_from_buffer()) == 10
- assert self.integration_buffer.is_integration_broken() is True
- self.sentry_app.refresh_from_db() # reload to get updated events
- assert self.sentry_app.events == events # check that events are the same / app is enabled
-
def test_notify_disabled_email(self):
with self.tasks():
notify_disable(
diff --git a/tests/sentry/utils/test_audit.py b/tests/sentry/utils/test_audit.py
index e8373acfb97c45..28f75902447146 100644
--- a/tests/sentry/utils/test_audit.py
+++ b/tests/sentry/utils/test_audit.py
@@ -301,6 +301,20 @@ def test_audit_entry_project_performance_setting_enable_detection(self):
== "edited project performance issue detector settings to enable detection of File IO on Main Thread issue"
)
+ def test_audit_entry_project_ownership_rule_edit(self):
+ entry = create_audit_entry(
+ request=self.req,
+ organization=self.org,
+ target_object=self.project.id,
+ event=audit_log.get_event_id("PROJECT_OWNERSHIPRULE_EDIT"),
+ )
+ audit_log_event = audit_log.get(entry.event)
+
+ assert entry.actor == self.user
+ assert entry.target_object == self.project.id
+ assert entry.event == audit_log.get_event_id("PROJECT_OWNERSHIPRULE_EDIT")
+ assert audit_log_event.render(entry) == "modified ownership rules"
+
def test_audit_entry_integration_log(self):
project = self.create_project()
self.login_as(user=self.user)
diff --git a/yarn.lock b/yarn.lock
index 0e4053f60ae0cb..9bb61015c881bc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5474,17 +5474,17 @@ eslint-config-prettier@^8.8.0:
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348"
integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==
-eslint-config-sentry-app@1.122.0:
- version "1.122.0"
- resolved "https://registry.yarnpkg.com/eslint-config-sentry-app/-/eslint-config-sentry-app-1.122.0.tgz#51f73659477be68a1102ea638582307e17d5e697"
- integrity sha512-+5d+VZK3ZwC02rvgOtwtGcoWUjdjOcE0XERsnIFAbIy2rCAJI01xu4U0h8/9nnc3ljxXAym7IeRZFyjycCHQkg==
+eslint-config-sentry-app@1.123.0:
+ version "1.123.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-sentry-app/-/eslint-config-sentry-app-1.123.0.tgz#b0ccb1d32d22673fa39aabdefe8f6e8a66d62268"
+ integrity sha512-rTvynJ94r+eYNl40me8UMrO+/AU2SruCYptzcW92daoXqh8z3rGPiljSBb1VuWCftU1e4ZFpydik9Zk1Zqj+zQ==
dependencies:
"@emotion/eslint-plugin" "^11.11.0"
"@typescript-eslint/eslint-plugin" "^6.2.0"
"@typescript-eslint/parser" "^6.2.0"
eslint-config-prettier "^8.8.0"
- eslint-config-sentry "^1.122.0"
- eslint-config-sentry-react "^1.122.0"
+ eslint-config-sentry "^1.123.0"
+ eslint-config-sentry-react "^1.123.0"
eslint-import-resolver-typescript "^2.7.1"
eslint-import-resolver-webpack "^0.13.2"
eslint-plugin-import "^2.27.5"
@@ -5492,24 +5492,24 @@ eslint-config-sentry-app@1.122.0:
eslint-plugin-no-lookahead-lookbehind-regexp "0.1.0"
eslint-plugin-prettier "^4.2.1"
eslint-plugin-react "^7.32.2"
- eslint-plugin-sentry "^1.122.0"
+ eslint-plugin-sentry "^1.123.0"
eslint-plugin-simple-import-sort "^10.0.0"
-eslint-config-sentry-react@^1.122.0:
- version "1.122.0"
- resolved "https://registry.yarnpkg.com/eslint-config-sentry-react/-/eslint-config-sentry-react-1.122.0.tgz#afdad711ab48bffc714d8901f11b8e3979260de8"
- integrity sha512-l74Io+JSUduvFW1EEkOZkGV9/cgrRvKZuFP9VGJ4rUmbYpKCemWc8IXUNU1PXanygrm8T3r7DzcIq8DuTnWRQA==
+eslint-config-sentry-react@^1.123.0:
+ version "1.123.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-sentry-react/-/eslint-config-sentry-react-1.123.0.tgz#120cd5f53f3fcffc77f7ea0de503713710a0d8df"
+ integrity sha512-88E4tFqx0tywmRjflKHdIszaTnBoaHUh8GwYYLi39Kl09SisfizdTx00+TovqUjHknrJiBGLH3XBXcKVr3DRQw==
dependencies:
- eslint-config-sentry "^1.122.0"
+ eslint-config-sentry "^1.123.0"
eslint-plugin-jest-dom "^5.0.1"
eslint-plugin-react-hooks "^4.6.0"
eslint-plugin-testing-library "^5.11.0"
eslint-plugin-typescript-sort-keys "^2.3.0"
-eslint-config-sentry@^1.122.0:
- version "1.122.0"
- resolved "https://registry.yarnpkg.com/eslint-config-sentry/-/eslint-config-sentry-1.122.0.tgz#4e8fc860decf088c747a6c951ce4ab3c4bbd5c10"
- integrity sha512-gHSOkyJw1ymGNy0hT7iBrrenr2aa3ITVTqllCE4mByWjvMuud5vd0qOED8/LPPPY5UsVhppBEC5Barm+g5Yfaw==
+eslint-config-sentry@^1.123.0:
+ version "1.123.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-sentry/-/eslint-config-sentry-1.123.0.tgz#f21946f974ccf431634cc9c89322261ec8a9a453"
+ integrity sha512-tI9cULUzADTcwUrWOGPbTSTmqbwIkYgz5WoznizO/Y1ISgVa8YGujZjUqROb63ob9eeAGlRRgelfdEKXkcHpLA==
eslint-import-resolver-node@^0.3.7:
version "0.3.7"
@@ -5632,10 +5632,10 @@ eslint-plugin-react@^7.32.2:
semver "^6.3.0"
string.prototype.matchall "^4.0.8"
-eslint-plugin-sentry@^1.122.0:
- version "1.122.0"
- resolved "https://registry.yarnpkg.com/eslint-plugin-sentry/-/eslint-plugin-sentry-1.122.0.tgz#39b48517fa714287320c29230bd5d67b67dbf53c"
- integrity sha512-1qG86KtKvYIa/mpxFF+fIIAJq3ZJalHJg9KfeoDVsrnJuB4McTo5djnu+VtCgCF278hwGk9ijk7th9KnIYQvDA==
+eslint-plugin-sentry@^1.123.0:
+ version "1.123.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-sentry/-/eslint-plugin-sentry-1.123.0.tgz#b7480350ff9f6ad0c56fd1ca6ff05e6d77216506"
+ integrity sha512-m7NVJeZb8kP3ErKbGMbYddxK6hqLhBZLr29B6qA7dmNbLujRqOzkANAb1BGR4zNmAcNpsHp9dAn9vapjApllSQ==
dependencies:
requireindex "~1.2.0"