Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,13 @@ export function useUpgradeNowParams({organization, subscription, enabled = true}
let events = currentHistory?.reserved ?? 0;

if (canCompare) {
const price = getBucket({events, buckets: eventBuckets}).price;
const eventsByPrice = getBucket({
price,
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
buckets: am2Plan.planCategories[category],
}).events;
const price = getBucket({events, buckets: eventBuckets})?.price ?? 0;
const eventsByPrice =
getBucket({
price,
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
buckets: am2Plan.planCategories[category],
})?.events ?? 0;
events = Math.max(events, eventsByPrice);
}
return [category, events];
Expand Down
3 changes: 3 additions & 0 deletions static/gsApp/views/amCheckout/components/volumeSliders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ export function VolumeSliders({
events: currentSliderValues[category],
buckets: activePlan.planCategories[category],
});
if (!eventBucket) {
return null;
}

const categoryInfo = getCategoryInfoFromPlural(category);

Expand Down
13 changes: 7 additions & 6 deletions static/gsApp/views/amCheckout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ function AMCheckout(props: Props) {
events: value,
buckets: plan.planCategories[category as DataCategory],
shouldMinimize: hasPartnerMigrationFeature(organization),
}).events,
})?.events ?? value,
])
);

Expand Down Expand Up @@ -390,11 +390,12 @@ function AMCheckout(props: Props) {
let events = (!isTrialPlan(planDetails.id) && currentHistory?.reserved) || 0;

if (canCompare) {
const price = getBucket({events, buckets: eventBuckets}).price;
const eventsByPrice = getBucket({
price,
buckets: initialPlan.planCategories[category],
}).events;
const price = getBucket({events, buckets: eventBuckets})?.price ?? 0;
const eventsByPrice =
getBucket({
price,
buckets: initialPlan.planCategories[category],
})?.events ?? 0;
events = Math.max(events, eventsByPrice);
}
return [category, events];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ export function ReserveAdditionalVolume({
)
.some(
({category, reserved}) =>
getBucket({
(getBucket({
buckets: activePlan.planCategories[category],
events: reserved ?? 0,
}).price > 0
})?.price ?? 0) > 0
)
);
const [reserved, setReserved] = useState<Partial<Record<DataCategory, number>>>(
Expand Down
60 changes: 40 additions & 20 deletions static/gsApp/views/amCheckout/utils.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ describe('utils', () => {
it('can get exact bucket by events', () => {
const events = 100_000;
const bucket = utils.getBucket({events, buckets: bizPlan.planCategories.errors});
expect(bucket.events).toBe(events);
expect(bucket).not.toBeNull();
expect(bucket!.events).toBe(events);
});

it('can get exact bucket by events with minimize strategy', () => {
Expand All @@ -103,13 +104,15 @@ describe('utils', () => {
buckets: bizPlan.planCategories.errors,
shouldMinimize: true,
});
expect(bucket.events).toBe(events);
expect(bucket).not.toBeNull();
expect(bucket!.events).toBe(events);
});

it('can get approximate bucket if event level does not exist', () => {
const events = 90_000;
const bucket = utils.getBucket({events, buckets: bizPlan.planCategories.errors});
expect(bucket.events).toBeGreaterThanOrEqual(events);
expect(bucket).not.toBeNull();
expect(bucket!.events).toBeGreaterThanOrEqual(events);
});

it('can get approximate bucket if event level does not exist with minimize strategy', () => {
Expand All @@ -119,7 +122,8 @@ describe('utils', () => {
buckets: bizPlan.planCategories.errors,
shouldMinimize: true,
});
expect(bucket.events).toBeLessThanOrEqual(events);
expect(bucket).not.toBeNull();
expect(bucket!.events).toBeLessThanOrEqual(events);
});

it('can get first bucket by events', () => {
Expand All @@ -128,7 +132,8 @@ describe('utils', () => {
events,
buckets: teamPlan.planCategories.transactions,
});
expect(bucket.events).toBeGreaterThanOrEqual(events);
expect(bucket).not.toBeNull();
expect(bucket!.events).toBeGreaterThanOrEqual(events);
});

it('can get first bucket by events with minimize strategy', () => {
Expand All @@ -138,7 +143,8 @@ describe('utils', () => {
buckets: teamPlan.planCategories.transactions,
shouldMinimize: true,
});
expect(bucket.events).toBeGreaterThanOrEqual(events);
expect(bucket).not.toBeNull();
expect(bucket!.events).toBeGreaterThanOrEqual(events);
});

it('can get last bucket by events', () => {
Expand All @@ -147,7 +153,8 @@ describe('utils', () => {
events,
buckets: teamPlan.planCategories.attachments,
});
expect(bucket.events).toBeLessThanOrEqual(events);
expect(bucket).not.toBeNull();
expect(bucket!.events).toBeLessThanOrEqual(events);
});

it('can get last bucket by events with minimize strategy', () => {
Expand All @@ -157,7 +164,8 @@ describe('utils', () => {
buckets: teamPlan.planCategories.attachments,
shouldMinimize: true,
});
expect(bucket.events).toBeLessThanOrEqual(events);
expect(bucket).not.toBeNull();
expect(bucket!.events).toBeLessThanOrEqual(events);
});

it('can get exact bucket by price', () => {
Expand All @@ -166,8 +174,9 @@ describe('utils', () => {
price,
buckets: bizPlan.planCategories.transactions,
});
expect(bucket.price).toBe(price);
expect(bucket.events).toBe(3_500_000);
expect(bucket).not.toBeNull();
expect(bucket!.price).toBe(price);
expect(bucket!.events).toBe(3_500_000);
});

it('can get exact bucket by price with minimize strategy', () => {
Expand All @@ -177,8 +186,9 @@ describe('utils', () => {
buckets: bizPlan.planCategories.transactions,
shouldMinimize: true,
});
expect(bucket.price).toBe(price);
expect(bucket.events).toBe(3_500_000);
expect(bucket).not.toBeNull();
expect(bucket!.price).toBe(price);
expect(bucket!.events).toBe(3_500_000);
});

it('can get approximate bucket if price level does not exist', () => {
Expand All @@ -187,8 +197,9 @@ describe('utils', () => {
price,
buckets: bizPlan.planCategories.transactions,
});
expect(bucket.price).toBeGreaterThanOrEqual(price);
expect(bucket.events).toBe(4_500_000);
expect(bucket).not.toBeNull();
expect(bucket!.price).toBeGreaterThanOrEqual(price);
expect(bucket!.events).toBe(4_500_000);
});

it('can get approximate bucket if price level does not exist with minimize strategy', () => {
Expand All @@ -198,8 +209,9 @@ describe('utils', () => {
buckets: bizPlan.planCategories.transactions,
shouldMinimize: true,
});
expect(bucket.price).toBeLessThanOrEqual(price);
expect(bucket.events).toBe(4_000_000);
expect(bucket).not.toBeNull();
expect(bucket!.price).toBeLessThanOrEqual(price);
expect(bucket!.events).toBe(4_000_000);
});

it('can get first bucket by price', () => {
Expand All @@ -208,7 +220,8 @@ describe('utils', () => {
price,
buckets: teamPlan.planCategories.transactions,
});
expect(bucket.price).toBe(price);
expect(bucket).not.toBeNull();
expect(bucket!.price).toBe(price);
});

it('can get first bucket by price with minimize strategy', () => {
Expand All @@ -218,7 +231,8 @@ describe('utils', () => {
buckets: teamPlan.planCategories.transactions,
shouldMinimize: true,
});
expect(bucket.price).toBe(price);
expect(bucket).not.toBeNull();
expect(bucket!.price).toBe(price);
});

it('can get last bucket by price', () => {
Expand All @@ -227,7 +241,8 @@ describe('utils', () => {
price,
buckets: teamPlan.planCategories.transactions,
});
expect(bucket.price).toBeLessThanOrEqual(price);
expect(bucket).not.toBeNull();
expect(bucket!.price).toBeLessThanOrEqual(price);
});

it('can get last bucket by price with minimize strategy', () => {
Expand All @@ -237,7 +252,12 @@ describe('utils', () => {
buckets: teamPlan.planCategories.transactions,
shouldMinimize: true,
});
expect(bucket.price).toBeLessThanOrEqual(price);
expect(bucket).not.toBeNull();
expect(bucket!.price).toBeLessThanOrEqual(price);
});

it('returns null when buckets are missing', () => {
expect(utils.getBucket({events: 1000, buckets: undefined})).toBeNull();
});
});

Expand Down
15 changes: 8 additions & 7 deletions static/gsApp/views/amCheckout/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,14 @@ export function getBucket({
events?: number;
price?: number;
shouldMinimize?: boolean; // the slot strategy when `events` does not exist in `buckets`
}): EventBucket {
}): EventBucket | null {
if (buckets) {
const slot = getSlot(events, price, buckets, shouldMinimize);
if (slot in buckets) {
return buckets[slot]!;
}
}
throw new Error('Invalid data category for plan');
return null;
}

type ReservedTotalProps = {
Expand Down Expand Up @@ -217,7 +217,7 @@ function getReservedPriceForReservedBudgetCategory({
events: RESERVED_BUDGET_QUOTA,
buckets: plan.planCategories[dataCategory],
});
return acc + bucket.price;
return acc + (bucket?.price ?? 0);
}, 0);
}

Expand Down Expand Up @@ -246,10 +246,11 @@ export function getReservedPriceCents({

Object.entries(reserved).forEach(
([category, quantity]) =>
(reservedCents += getBucket({
events: quantity,
buckets: plan.planCategories[category as DataCategory],
}).price)
(reservedCents +=
getBucket({
events: quantity,
buckets: plan.planCategories[category as DataCategory],
})?.price ?? 0)
);

Object.entries(addOns ?? {}).forEach(([apiName, {enabled}]) => {
Expand Down
2 changes: 1 addition & 1 deletion static/gsApp/views/spendLimits/spendLimitSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ function getPaygPpe({
events: reserved === RESERVED_BUDGET_QUOTA ? reserved : reserved + 1, // +1 to get the next bucket, if any
shouldMinimize: false,
});
return bucket.onDemandPrice ?? 0;
return bucket?.onDemandPrice ?? 0;
}

function SpendLimitInput({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ function calculateCategoryPrepaidUsage(
// This will be 0 when they are using the included amount
const prepaidPrice = hasReservedBudget
? prepaid
: (prepaidPriceBucket.price ?? 0) / (isMonthly ? 1 : 12);
: (prepaidPriceBucket?.price ?? 0) / (isMonthly ? 1 : 12);

// Calculate spend based on percentage used
const prepaidSpend = (prepaidPercentUsed / 100) * prepaidPrice;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import moment from 'moment-timezone';
import {OrganizationFixture} from 'sentry-fixture/organization';

import {CustomerUsageFixture} from 'getsentry-test/fixtures/customerUsage';
import {MetricHistoryFixture} from 'getsentry-test/fixtures/metricHistory';
import {
SubscriptionFixture,
SubscriptionWithLegacySeerFixture,
Expand Down Expand Up @@ -498,4 +499,31 @@ describe('UsageOverviewTable', () => {
// All disabled rows must appear after all enabled rows
expect(lastEnabledIndex).toBeLessThan(firstDisabledIndex);
});

it('renders categories that are missing plan buckets without crashing', async () => {
const sub = SubscriptionFixture({organization, plan: 'am2_business'});
sub.categories.spansIndexed = MetricHistoryFixture({
category: DataCategory.SPANS_INDEXED,
reserved: 1000,
prepaid: 1000,
usage: 500,
order: 99,
});
SubscriptionStore.set(organization.slug, sub);

render(
<UsageOverviewTable
subscription={sub}
organization={organization}
usageData={usageData}
onRowClick={jest.fn()}
selectedProduct={DataCategory.ERRORS}
/>
);

await screen.findByRole('columnheader', {name: 'Feature'});

expect(screen.getByTestId('product-row-errors')).toBeInTheDocument();
expect(screen.getByTestId('product-row-spansIndexed')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,16 @@ export function UsageOverviewTableRow({

paygSpend = normalizedMetricHistory.onDemandSpendUsed ?? 0;
}
const bucket = getBucket({
events: reserved ?? 0, // buckets use the converted unit reserved amount (ie. in GB for byte categories)
buckets: subscription.planDetails.planCategories[billedCategory],
});
const buckets = subscription.planDetails.planCategories[billedCategory];
const bucket =
buckets && buckets.length > 0
? getBucket({
events: reserved ?? 0,
buckets,
})
: null;
otherSpend = calculateSeerUserSpend(normalizedMetricHistory);
const recurringReservedSpend = isChildProduct ? 0 : (bucket.price ?? 0);
const recurringReservedSpend = isChildProduct ? 0 : (bucket?.price ?? 0);
const additionalSpend = recurringReservedSpend + paygSpend + otherSpend;

const formattedSoftCapType =
Expand Down
18 changes: 14 additions & 4 deletions static/gsApp/views/subscriptionPage/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,20 @@ export function calculateCategorySpend(
}

const priceBucket = getBucket({events: categoryInfo.reserved, buckets: slots});
const eventsByPrice = getBucket({
price: priceBucket.price,
buckets: slots,
}).events;
if (!priceBucket) {
return {
prepaidSpent: 0,
onDemandSpent: 0,
unitPrice: 0,
onDemandUnitPrice: 0,
prepaidPrice: 0,
};
}
const eventsByPrice =
getBucket({
price: priceBucket.price,
buckets: slots,
})?.events ?? 0;

const unitPrice = priceBucket.unitPrice ?? 0;
// Subtract gifted usage from total usage
Expand Down
Loading