Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(billing): Add profile hours UI to user spend notifications page #87350

Merged
merged 5 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/sentry/features/permanent.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ def register_permanent_features(manager: FeatureManager):
"organizations:team-roles": True,
# Enable the uptime monitoring features
"organizations:uptime": True,
# Feature flag for continuous profiling billing-related features.
# Separate from organizations:continuous-profiling feature flag.
"organizations:continuous-profiling-billing": False,
# Signals that the organization supports the on demand metrics prefill.
"organizations:on-demand-metrics-prefill": False,
# Metrics: Enable ingestion and storage of custom metrics. See custom-metrics for UI.
Expand Down
4 changes: 2 additions & 2 deletions static/app/constants/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ export const DATA_CATEGORY_INFO = {
titleName: t('Profile Hours'),
productName: t('Continuous Profiling'),
uid: 17,
isBilledCategory: false, // TODO(Continuous Profiling GA): make true for launch to show spend notification toggle
isBilledCategory: true,
},
[DataCategoryExact.PROFILE_DURATION_UI]: {
name: DataCategoryExact.PROFILE_DURATION_UI,
Expand All @@ -385,7 +385,7 @@ export const DATA_CATEGORY_INFO = {
titleName: t('UI Profile Hours'),
productName: t('UI Profiling'),
uid: 25,
isBilledCategory: false, // TODO(Continuous Profiling GA): make true for launch to show spend notification toggle
isBilledCategory: true,
},
[DataCategoryExact.UPTIME]: {
name: DataCategoryExact.UPTIME,
Expand Down
27 changes: 13 additions & 14 deletions static/app/utils/theme/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import type {CSSProperties} from 'react';
import {css} from '@emotion/react';
import color from 'color';

import {DATA_CATEGORY_INFO} from 'sentry/constants';
import {CHART_PALETTE} from 'sentry/constants/chartPalette';
import {type DataCategory, Outcome} from 'sentry/types/core';
import {DataCategory, Outcome} from 'sentry/types/core';

export const generateThemeAliases = (colors: Colors) => ({
/**
Expand Down Expand Up @@ -911,21 +910,21 @@ const iconSizes: Sizes = {
const dataCategory: Record<
Exclude<
DataCategory,
| 'profiles'
| 'profileChunks'
| 'profileDuration'
| 'profileDurationUI'
| 'spans'
| 'spansIndexed'
| 'uptime'
| DataCategory.PROFILES
| DataCategory.PROFILE_CHUNKS
| DataCategory.PROFILE_DURATION
| DataCategory.PROFILE_DURATION_UI
| DataCategory.SPANS
| DataCategory.SPANS_INDEXED
| DataCategory.UPTIME
>,
string
> = {
[DATA_CATEGORY_INFO.error.plural]: CHART_PALETTE[4][3],
[DATA_CATEGORY_INFO.transaction.plural]: CHART_PALETTE[4][2],
[DATA_CATEGORY_INFO.attachment.plural]: CHART_PALETTE[4][1],
[DATA_CATEGORY_INFO.replay.plural]: CHART_PALETTE[4][4],
[DATA_CATEGORY_INFO.monitorSeat.plural]: '#a397f7',
[DataCategory.ERRORS]: CHART_PALETTE[4][3],
[DataCategory.TRANSACTIONS]: CHART_PALETTE[4][2],
[DataCategory.ATTACHMENTS]: CHART_PALETTE[4][1],
[DataCategory.REPLAYS]: CHART_PALETTE[4][4],
[DataCategory.MONITOR_SEATS]: '#a397f7',
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,9 +318,13 @@ describe('NotificationSettingsByType', function () {
});

it('spend notifications on org with am3 with spend visibility notifications', async function () {
const organization = OrganizationFixture();
organization.features.push('spend-visibility-notifications');
organization.features.push('am3-tier');
const organization = OrganizationFixture({
features: [
'spend-visibility-notifications',
'am3-tier',
'continuous-profiling-billing',
],
});
renderComponent({
notificationType: 'quota',
organizations: [organization],
Expand All @@ -333,7 +337,8 @@ describe('NotificationSettingsByType', function () {
expect(screen.getByText('Session Replays')).toBeInTheDocument();
expect(screen.getByText('Attachments')).toBeInTheDocument();
expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
expect(screen.queryByText('Continuous Profiling')).not.toBeInTheDocument(); // TODO(Continuous Profiling GA): should be in document
expect(screen.getByText('Profile Hours', {exact: true})).toBeInTheDocument();
expect(screen.getByText('UI Profile Hours', {exact: true})).toBeInTheDocument();
expect(screen.queryByText('Transactions')).not.toBeInTheDocument();

const editSettingMock = MockApiClient.addMockResponse({
Expand Down Expand Up @@ -366,9 +371,13 @@ describe('NotificationSettingsByType', function () {
});

it('spend notifications on org with am3 and org without am3', async function () {
const organization = OrganizationFixture();
organization.features.push('spend-visibility-notifications');
organization.features.push('am3-tier');
const organization = OrganizationFixture({
features: [
'spend-visibility-notifications',
'am3-tier',
'continuous-profiling-billing',
],
});
const otherOrganization = OrganizationFixture();
renderComponent({
notificationType: 'quota',
Expand All @@ -383,13 +392,15 @@ describe('NotificationSettingsByType', function () {
expect(screen.getByText('Attachments')).toBeInTheDocument();
expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
expect(screen.getByText('Transactions')).toBeInTheDocument();
expect(screen.queryByText('Continuous Profiling')).not.toBeInTheDocument(); // TODO(Continuous Profiling GA): should be in document
expect(screen.getByText('Profile Hours', {exact: true})).toBeInTheDocument();
expect(screen.getByText('UI Profile Hours', {exact: true})).toBeInTheDocument();
});

it('spend notifications on org with am1 org only', async function () {
const organization = OrganizationFixture();
organization.features.push('spend-visibility-notifications');
organization.features.push('am1-tier');
organization.features.push('continuous-profiling-billing');
const otherOrganization = OrganizationFixture();
renderComponent({
notificationType: 'quota',
Expand All @@ -403,13 +414,15 @@ describe('NotificationSettingsByType', function () {
expect(screen.getByText('Attachments')).toBeInTheDocument();
expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
expect(screen.getByText('Transactions')).toBeInTheDocument();
expect(screen.queryByText('Continuous Profiling')).not.toBeInTheDocument();
expect(screen.queryByText('Profile Hours', {exact: true})).not.toBeInTheDocument();
expect(screen.queryByText('UI Profile Hours', {exact: true})).not.toBeInTheDocument();
expect(screen.queryByText('Spans')).not.toBeInTheDocument();
});

it('spend notifications on org with am3 without spend visibility notifications', async function () {
const organization = OrganizationFixture();
organization.features.push('am3-tier');
const organization = OrganizationFixture({
features: ['am3-tier', 'continuous-profiling-billing'],
});
renderComponent({
notificationType: 'quota',
organizations: [organization],
Expand All @@ -423,7 +436,8 @@ describe('NotificationSettingsByType', function () {
expect(screen.getByText('Session Replays')).toBeInTheDocument();
expect(screen.getByText('Attachments')).toBeInTheDocument();
expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
expect(screen.queryByText('Continuous Profiling')).not.toBeInTheDocument(); // TODO(Continuous Profiling GA): should be in document
expect(screen.getByText('Profile Hours', {exact: true})).toBeInTheDocument();
expect(screen.getByText('UI Profile Hours', {exact: true})).toBeInTheDocument();
expect(screen.queryByText('Transactions')).not.toBeInTheDocument();

const editSettingMock = MockApiClient.addMockResponse({
Expand Down Expand Up @@ -454,4 +468,32 @@ describe('NotificationSettingsByType', function () {
})
);
});

it('should not show Profile Hours when continuous-profiling-billing is not enabled', async function () {
const organization = OrganizationFixture({
features: [
'spend-visibility-notifications',
'am3-tier',
// No continuous-profiling-billing feature
],
});
renderComponent({
notificationType: 'quota',
organizations: [organization],
});

expect(await screen.getAllByText('Spend Notifications').length).toBe(2);

// These should be present
expect(screen.getByText('Errors')).toBeInTheDocument();
expect(screen.getByText('Spans')).toBeInTheDocument();
expect(screen.getByText('Session Replays')).toBeInTheDocument();
expect(screen.getByText('Attachments')).toBeInTheDocument();
expect(screen.getByText('Spend Allocations')).toBeInTheDocument();

// These should NOT be present
expect(screen.queryByText('Profile Hours', {exact: true})).not.toBeInTheDocument();
expect(screen.queryByText('UI Profile Hours', {exact: true})).not.toBeInTheDocument();
expect(screen.queryByText('Transactions')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,15 @@ class NotificationSettingsByTypeV2 extends DeprecatedAsyncComponent<Props, State
organization.features?.includes('am2-tier')
);

// Check if any organization has the continuous-profiling-billing feature flag
const hasOrgWithContinuousProfilingBilling = organizations.some(organization =>
organization.features?.includes('continuous-profiling-billing')
);

const excludeTransactions = hasOrgWithAm3 && !hasOrgWithoutAm3;
const includeSpans = hasOrgWithAm3;
const includeProfileDuration = hasOrgWithAm2 || hasOrgWithAm3;
const includeProfileDuration =
(hasOrgWithAm2 || hasOrgWithAm3) && hasOrgWithContinuousProfilingBilling;

// if a quota notification is not disabled, add in our dependent fields
// but do not show the top level controller
Expand All @@ -219,7 +225,10 @@ class NotificationSettingsByTypeV2 extends DeprecatedAsyncComponent<Props, State
if (field.name === 'quotaTransactions' && excludeTransactions) {
return false;
}
if (field.name === 'quotaProfileDuration' && !includeProfileDuration) {
if (
['quotaProfileDuration', 'quotaProfileDurationUI'].includes(field.name) &&
!includeProfileDuration
) {
return false;
}
return true;
Expand All @@ -246,7 +255,10 @@ class NotificationSettingsByTypeV2 extends DeprecatedAsyncComponent<Props, State
if (field.name === 'quotaTransactions' && excludeTransactions) {
return false;
}
if (field.name === 'quotaProfileDuration' && !includeProfileDuration) {
if (
['quotaProfileDuration', 'quotaProfileDurationUI'].includes(field.name) &&
!includeProfileDuration
) {
return false;
}
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export function getDocsLinkForEventType(
case DataCategoryExact.MONITOR_SEAT:
return 'https://docs.sentry.io/product/crons/';
case DataCategoryExact.PROFILE_DURATION:
case DataCategoryExact.PROFILE_DURATION_UI:
return 'https://docs.sentry.io/product/explore/profiling/';
case DataCategoryExact.UPTIME:
return 'https://docs.sentry.io/product/alerts/uptime-monitoring/';
Expand Down
Loading