Skip to content

Commit fa6e6a6

Browse files
authored
feat(billing): Add profile hours UI to user spend notifications page (#87350)
Closes getsentry/getsentry#16818 This adds continuous profiling to the spend notifications page on the user's personal notifications preferences. This also adds the `organizations:continuous-profiling-billing` feature so that we can feature flag billing-related features until launch. The newly added feature is a follow up to #80787. This is intentionally separate from `organizations:continuous-profiling` feature flag.
1 parent e7d0bbb commit fa6e6a6

File tree

6 files changed

+88
-31
lines changed

6 files changed

+88
-31
lines changed

src/sentry/features/permanent.py

+3
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ def register_permanent_features(manager: FeatureManager):
108108
"organizations:team-roles": True,
109109
# Enable the uptime monitoring features
110110
"organizations:uptime": True,
111+
# Feature flag for continuous profiling billing-related features.
112+
# Separate from organizations:continuous-profiling feature flag.
113+
"organizations:continuous-profiling-billing": False,
111114
# Signals that the organization supports the on demand metrics prefill.
112115
"organizations:on-demand-metrics-prefill": False,
113116
# Metrics: Enable ingestion and storage of custom metrics. See custom-metrics for UI.

static/app/constants/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ export const DATA_CATEGORY_INFO = {
375375
titleName: t('Profile Hours'),
376376
productName: t('Continuous Profiling'),
377377
uid: 17,
378-
isBilledCategory: false, // TODO(Continuous Profiling GA): make true for launch to show spend notification toggle
378+
isBilledCategory: true,
379379
},
380380
[DataCategoryExact.PROFILE_DURATION_UI]: {
381381
name: DataCategoryExact.PROFILE_DURATION_UI,
@@ -385,7 +385,7 @@ export const DATA_CATEGORY_INFO = {
385385
titleName: t('UI Profile Hours'),
386386
productName: t('UI Profiling'),
387387
uid: 25,
388-
isBilledCategory: false, // TODO(Continuous Profiling GA): make true for launch to show spend notification toggle
388+
isBilledCategory: true,
389389
},
390390
[DataCategoryExact.UPTIME]: {
391391
name: DataCategoryExact.UPTIME,

static/app/utils/theme/theme.tsx

+13-14
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ import type {CSSProperties} from 'react';
1111
import {css} from '@emotion/react';
1212
import color from 'color';
1313

14-
import {DATA_CATEGORY_INFO} from 'sentry/constants';
1514
import {CHART_PALETTE} from 'sentry/constants/chartPalette';
16-
import {type DataCategory, Outcome} from 'sentry/types/core';
15+
import {DataCategory, Outcome} from 'sentry/types/core';
1716

1817
export const generateThemeAliases = (colors: Colors) => ({
1918
/**
@@ -911,21 +910,21 @@ const iconSizes: Sizes = {
911910
const dataCategory: Record<
912911
Exclude<
913912
DataCategory,
914-
| 'profiles'
915-
| 'profileChunks'
916-
| 'profileDuration'
917-
| 'profileDurationUI'
918-
| 'spans'
919-
| 'spansIndexed'
920-
| 'uptime'
913+
| DataCategory.PROFILES
914+
| DataCategory.PROFILE_CHUNKS
915+
| DataCategory.PROFILE_DURATION
916+
| DataCategory.PROFILE_DURATION_UI
917+
| DataCategory.SPANS
918+
| DataCategory.SPANS_INDEXED
919+
| DataCategory.UPTIME
921920
>,
922921
string
923922
> = {
924-
[DATA_CATEGORY_INFO.error.plural]: CHART_PALETTE[4][3],
925-
[DATA_CATEGORY_INFO.transaction.plural]: CHART_PALETTE[4][2],
926-
[DATA_CATEGORY_INFO.attachment.plural]: CHART_PALETTE[4][1],
927-
[DATA_CATEGORY_INFO.replay.plural]: CHART_PALETTE[4][4],
928-
[DATA_CATEGORY_INFO.monitorSeat.plural]: '#a397f7',
923+
[DataCategory.ERRORS]: CHART_PALETTE[4][3],
924+
[DataCategory.TRANSACTIONS]: CHART_PALETTE[4][2],
925+
[DataCategory.ATTACHMENTS]: CHART_PALETTE[4][1],
926+
[DataCategory.REPLAYS]: CHART_PALETTE[4][4],
927+
[DataCategory.MONITOR_SEATS]: '#a397f7',
929928
};
930929

931930
/**

static/app/views/settings/account/notifications/notificationSettingsByType.spec.tsx

+54-12
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,13 @@ describe('NotificationSettingsByType', function () {
318318
});
319319

320320
it('spend notifications on org with am3 with spend visibility notifications', async function () {
321-
const organization = OrganizationFixture();
322-
organization.features.push('spend-visibility-notifications');
323-
organization.features.push('am3-tier');
321+
const organization = OrganizationFixture({
322+
features: [
323+
'spend-visibility-notifications',
324+
'am3-tier',
325+
'continuous-profiling-billing',
326+
],
327+
});
324328
renderComponent({
325329
notificationType: 'quota',
326330
organizations: [organization],
@@ -333,7 +337,8 @@ describe('NotificationSettingsByType', function () {
333337
expect(screen.getByText('Session Replays')).toBeInTheDocument();
334338
expect(screen.getByText('Attachments')).toBeInTheDocument();
335339
expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
336-
expect(screen.queryByText('Continuous Profiling')).not.toBeInTheDocument(); // TODO(Continuous Profiling GA): should be in document
340+
expect(screen.getByText('Profile Hours', {exact: true})).toBeInTheDocument();
341+
expect(screen.getByText('UI Profile Hours', {exact: true})).toBeInTheDocument();
337342
expect(screen.queryByText('Transactions')).not.toBeInTheDocument();
338343

339344
const editSettingMock = MockApiClient.addMockResponse({
@@ -366,9 +371,13 @@ describe('NotificationSettingsByType', function () {
366371
});
367372

368373
it('spend notifications on org with am3 and org without am3', async function () {
369-
const organization = OrganizationFixture();
370-
organization.features.push('spend-visibility-notifications');
371-
organization.features.push('am3-tier');
374+
const organization = OrganizationFixture({
375+
features: [
376+
'spend-visibility-notifications',
377+
'am3-tier',
378+
'continuous-profiling-billing',
379+
],
380+
});
372381
const otherOrganization = OrganizationFixture();
373382
renderComponent({
374383
notificationType: 'quota',
@@ -383,13 +392,15 @@ describe('NotificationSettingsByType', function () {
383392
expect(screen.getByText('Attachments')).toBeInTheDocument();
384393
expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
385394
expect(screen.getByText('Transactions')).toBeInTheDocument();
386-
expect(screen.queryByText('Continuous Profiling')).not.toBeInTheDocument(); // TODO(Continuous Profiling GA): should be in document
395+
expect(screen.getByText('Profile Hours', {exact: true})).toBeInTheDocument();
396+
expect(screen.getByText('UI Profile Hours', {exact: true})).toBeInTheDocument();
387397
});
388398

389399
it('spend notifications on org with am1 org only', async function () {
390400
const organization = OrganizationFixture();
391401
organization.features.push('spend-visibility-notifications');
392402
organization.features.push('am1-tier');
403+
organization.features.push('continuous-profiling-billing');
393404
const otherOrganization = OrganizationFixture();
394405
renderComponent({
395406
notificationType: 'quota',
@@ -403,13 +414,15 @@ describe('NotificationSettingsByType', function () {
403414
expect(screen.getByText('Attachments')).toBeInTheDocument();
404415
expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
405416
expect(screen.getByText('Transactions')).toBeInTheDocument();
406-
expect(screen.queryByText('Continuous Profiling')).not.toBeInTheDocument();
417+
expect(screen.queryByText('Profile Hours', {exact: true})).not.toBeInTheDocument();
418+
expect(screen.queryByText('UI Profile Hours', {exact: true})).not.toBeInTheDocument();
407419
expect(screen.queryByText('Spans')).not.toBeInTheDocument();
408420
});
409421

410422
it('spend notifications on org with am3 without spend visibility notifications', async function () {
411-
const organization = OrganizationFixture();
412-
organization.features.push('am3-tier');
423+
const organization = OrganizationFixture({
424+
features: ['am3-tier', 'continuous-profiling-billing'],
425+
});
413426
renderComponent({
414427
notificationType: 'quota',
415428
organizations: [organization],
@@ -423,7 +436,8 @@ describe('NotificationSettingsByType', function () {
423436
expect(screen.getByText('Session Replays')).toBeInTheDocument();
424437
expect(screen.getByText('Attachments')).toBeInTheDocument();
425438
expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
426-
expect(screen.queryByText('Continuous Profiling')).not.toBeInTheDocument(); // TODO(Continuous Profiling GA): should be in document
439+
expect(screen.getByText('Profile Hours', {exact: true})).toBeInTheDocument();
440+
expect(screen.getByText('UI Profile Hours', {exact: true})).toBeInTheDocument();
427441
expect(screen.queryByText('Transactions')).not.toBeInTheDocument();
428442

429443
const editSettingMock = MockApiClient.addMockResponse({
@@ -454,4 +468,32 @@ describe('NotificationSettingsByType', function () {
454468
})
455469
);
456470
});
471+
472+
it('should not show Profile Hours when continuous-profiling-billing is not enabled', async function () {
473+
const organization = OrganizationFixture({
474+
features: [
475+
'spend-visibility-notifications',
476+
'am3-tier',
477+
// No continuous-profiling-billing feature
478+
],
479+
});
480+
renderComponent({
481+
notificationType: 'quota',
482+
organizations: [organization],
483+
});
484+
485+
expect(await screen.getAllByText('Spend Notifications').length).toBe(2);
486+
487+
// These should be present
488+
expect(screen.getByText('Errors')).toBeInTheDocument();
489+
expect(screen.getByText('Spans')).toBeInTheDocument();
490+
expect(screen.getByText('Session Replays')).toBeInTheDocument();
491+
expect(screen.getByText('Attachments')).toBeInTheDocument();
492+
expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
493+
494+
// These should NOT be present
495+
expect(screen.queryByText('Profile Hours', {exact: true})).not.toBeInTheDocument();
496+
expect(screen.queryByText('UI Profile Hours', {exact: true})).not.toBeInTheDocument();
497+
expect(screen.queryByText('Transactions')).not.toBeInTheDocument();
498+
});
457499
});

static/app/views/settings/account/notifications/notificationSettingsByType.tsx

+15-3
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,15 @@ class NotificationSettingsByTypeV2 extends DeprecatedAsyncComponent<Props, State
199199
organization.features?.includes('am2-tier')
200200
);
201201

202+
// Check if any organization has the continuous-profiling-billing feature flag
203+
const hasOrgWithContinuousProfilingBilling = organizations.some(organization =>
204+
organization.features?.includes('continuous-profiling-billing')
205+
);
206+
202207
const excludeTransactions = hasOrgWithAm3 && !hasOrgWithoutAm3;
203208
const includeSpans = hasOrgWithAm3;
204-
const includeProfileDuration = hasOrgWithAm2 || hasOrgWithAm3;
209+
const includeProfileDuration =
210+
(hasOrgWithAm2 || hasOrgWithAm3) && hasOrgWithContinuousProfilingBilling;
205211

206212
// if a quota notification is not disabled, add in our dependent fields
207213
// but do not show the top level controller
@@ -219,7 +225,10 @@ class NotificationSettingsByTypeV2 extends DeprecatedAsyncComponent<Props, State
219225
if (field.name === 'quotaTransactions' && excludeTransactions) {
220226
return false;
221227
}
222-
if (field.name === 'quotaProfileDuration' && !includeProfileDuration) {
228+
if (
229+
['quotaProfileDuration', 'quotaProfileDurationUI'].includes(field.name) &&
230+
!includeProfileDuration
231+
) {
223232
return false;
224233
}
225234
return true;
@@ -246,7 +255,10 @@ class NotificationSettingsByTypeV2 extends DeprecatedAsyncComponent<Props, State
246255
if (field.name === 'quotaTransactions' && excludeTransactions) {
247256
return false;
248257
}
249-
if (field.name === 'quotaProfileDuration' && !includeProfileDuration) {
258+
if (
259+
['quotaProfileDuration', 'quotaProfileDurationUI'].includes(field.name) &&
260+
!includeProfileDuration
261+
) {
250262
return false;
251263
}
252264
return true;

static/app/views/settings/account/notifications/utils.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export function getDocsLinkForEventType(
5656
case DataCategoryExact.MONITOR_SEAT:
5757
return 'https://docs.sentry.io/product/crons/';
5858
case DataCategoryExact.PROFILE_DURATION:
59+
case DataCategoryExact.PROFILE_DURATION_UI:
5960
return 'https://docs.sentry.io/product/explore/profiling/';
6061
case DataCategoryExact.UPTIME:
6162
return 'https://docs.sentry.io/product/alerts/uptime-monitoring/';

0 commit comments

Comments
 (0)