Skip to content

Commit a137a20

Browse files
feat(seer): _admin product trial management (#91690)
Closes https://linear.app/getsentry/issue/BIL-546/manage-seer-product-trials <img width="611" alt="Screenshot 2025-05-14 at 5 53 52 PM" src="https://github.com/user-attachments/assets/3fb31a87-7b35-42bf-9f84-e20771cd32e2" />
1 parent e9eef76 commit a137a20

File tree

2 files changed

+66
-42
lines changed

2 files changed

+66
-42
lines changed

static/gsAdmin/components/customers/customerOverview.spec.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ describe('CustomerOverview', function () {
364364
expect(screen.queryByText('Spans:')).not.toBeInTheDocument();
365365
expect(screen.queryByText('Performance Units:')).not.toBeInTheDocument();
366366
expect(screen.queryByText('Transactions:')).not.toBeInTheDocument();
367+
expect(screen.queryByText('Seer:')).not.toBeInTheDocument();
367368
});
368369

369370
it('renders no product trials for non-self-serve account', function () {
@@ -388,6 +389,7 @@ describe('CustomerOverview', function () {
388389
expect(screen.queryByText('Spans:')).not.toBeInTheDocument();
389390
expect(screen.queryByText('Performance Units:')).not.toBeInTheDocument();
390391
expect(screen.queryByText('Transactions:')).not.toBeInTheDocument();
392+
expect(screen.queryByText('Seer:')).not.toBeInTheDocument();
391393
});
392394

393395
it('render product trials for am1 account', function () {
@@ -399,6 +401,7 @@ describe('CustomerOverview', function () {
399401
canSelfServe: true,
400402
});
401403
am1_subscription.planDetails.categories = [DataCategory.TRANSACTIONS];
404+
am1_subscription.planDetails.availableReservedBudgetTypes = {};
402405

403406
render(
404407
<CustomerOverview
@@ -447,6 +450,7 @@ describe('CustomerOverview', function () {
447450
).not.toBeInTheDocument();
448451

449452
expect(within(productTrialsList).queryByText('Spans:')).not.toBeInTheDocument();
453+
expect(within(productTrialsList).queryByText('Seer:')).not.toBeInTheDocument();
450454
});
451455

452456
it('render product trials for am2 account', function () {
@@ -461,6 +465,7 @@ describe('CustomerOverview', function () {
461465
DataCategory.REPLAYS,
462466
DataCategory.TRANSACTIONS,
463467
];
468+
am2_subscription.planDetails.availableReservedBudgetTypes = {};
464469

465470
render(
466471
<CustomerOverview
@@ -520,6 +525,7 @@ describe('CustomerOverview', function () {
520525
).not.toBeInTheDocument();
521526

522527
expect(within(productTrialsList).queryByText('Spans:')).not.toBeInTheDocument();
528+
expect(within(productTrialsList).queryByText('Seer:')).not.toBeInTheDocument();
523529
});
524530

525531
it('render product trials for am3 account', function () {
@@ -576,6 +582,17 @@ describe('CustomerOverview', function () {
576582
within(spansDefinition).getByRole('button', {name: 'Allow Trial'})
577583
).toBeInTheDocument();
578584

585+
// Check within the Seer section
586+
const seerTermElement = within(productTrialsList).getByText('Seer:');
587+
const seerDefinition = seerTermElement.nextElementSibling;
588+
expect(seerDefinition).toBeInTheDocument(); // Ensure we found the dd
589+
if (!seerDefinition || !(seerDefinition instanceof HTMLElement)) {
590+
throw new Error('Seer definition not found or not an HTMLElement');
591+
}
592+
expect(
593+
within(seerDefinition).getByRole('button', {name: 'Allow Trial'})
594+
).toBeInTheDocument();
595+
579596
// Ensure other trial categories are NOT present
580597
expect(
581598
within(productTrialsList).queryByText('Performance Units:')

static/gsAdmin/components/customers/customerOverview.tsx

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import ConfigStore from 'sentry/stores/configStore';
1212
import {DataCategory} from 'sentry/types/core';
1313
import type {Organization} from 'sentry/types/organization';
1414
import {defined} from 'sentry/utils';
15+
import {toTitleCase} from 'sentry/utils/string/toTitleCase';
1516
import useApi from 'sentry/utils/useApi';
1617

1718
import ChangeARRAction from 'admin/components/changeARRAction';
@@ -445,6 +446,12 @@ function CustomerOverview({customer, onAction, organization}: Props) {
445446
)
446447
: [];
447448

449+
const productTrialCategoryGroups = customer.canSelfServe
450+
? Object.values(customer.planDetails.availableReservedBudgetTypes).filter(
451+
group => group.canProductTrial
452+
)
453+
: [];
454+
448455
function updateCustomerStatus(action: string, type: string) {
449456
const data = {
450457
[action]: true,
@@ -462,6 +469,43 @@ function CustomerOverview({customer, onAction, organization}: Props) {
462469
});
463470
}
464471

472+
const getTrialManagementActions = (apiName: string, trialName: string) => {
473+
const formattedApiName = upperFirst(apiName);
474+
return (
475+
<DetailLabel
476+
key={apiName}
477+
title={toTitleCase(trialName, {allowInnerUpperCase: true})}
478+
>
479+
<TrialActions>
480+
<Button
481+
size="xs"
482+
onClick={() =>
483+
updateCustomerStatus(`allowTrial${formattedApiName}`, 'product trial')
484+
}
485+
>
486+
Allow Trial
487+
</Button>
488+
<Button
489+
size="xs"
490+
onClick={() =>
491+
updateCustomerStatus(`startTrial${formattedApiName}`, 'product trial')
492+
}
493+
>
494+
Start Trial
495+
</Button>
496+
<Button
497+
size="xs"
498+
onClick={() =>
499+
updateCustomerStatus(`stopTrial${formattedApiName}`, 'product trial')
500+
}
501+
>
502+
Stop Trial
503+
</Button>
504+
</TrialActions>
505+
</DetailLabel>
506+
);
507+
};
508+
465509
return (
466510
<DetailsContainer>
467511
<div>
@@ -642,7 +686,7 @@ function CustomerOverview({customer, onAction, organization}: Props) {
642686
</ExternalLink>
643687
</DetailLabel>
644688
</DetailList>
645-
{productTrialCategories.length > 0 && (
689+
{productTrialCategories.length + productTrialCategoryGroups.length > 0 && (
646690
<Fragment>
647691
<h6>Product Trials</h6>
648692
<ProductTrialsDetailListContainer>
@@ -652,47 +696,10 @@ function CustomerOverview({customer, onAction, organization}: Props) {
652696
category: categoryInfo.plural,
653697
title: true,
654698
});
655-
const upperCategory = upperFirst(categoryInfo.plural);
656-
657-
return (
658-
<DetailLabel key={categoryInfo.plural} title={categoryName}>
659-
<TrialActions>
660-
<Button
661-
size="xs"
662-
onClick={() =>
663-
updateCustomerStatus(
664-
`allowTrial${upperCategory}`,
665-
'product trial'
666-
)
667-
}
668-
>
669-
Allow Trial
670-
</Button>
671-
<Button
672-
size="xs"
673-
onClick={() =>
674-
updateCustomerStatus(
675-
`startTrial${upperCategory}`,
676-
'product trial'
677-
)
678-
}
679-
>
680-
Start Trial
681-
</Button>
682-
<Button
683-
size="xs"
684-
onClick={() =>
685-
updateCustomerStatus(
686-
`stopTrial${upperCategory}`,
687-
'product trial'
688-
)
689-
}
690-
>
691-
Stop Trial
692-
</Button>
693-
</TrialActions>
694-
</DetailLabel>
695-
);
699+
return getTrialManagementActions(categoryInfo.plural, categoryName);
700+
})}
701+
{productTrialCategoryGroups.map(group => {
702+
return getTrialManagementActions(group.apiName, group.productName);
696703
})}
697704
</ProductTrialsDetailListContainer>
698705
</Fragment>

0 commit comments

Comments
 (0)