123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- import pick from 'lodash/pick';
- import {DataCategory} from 'sentry/types/core';
- import type {Organization} from 'sentry/types/organization';
- import getDaysSinceDate from 'sentry/utils/getDaysSinceDate';
- import getOrganizationAge from 'sentry/utils/getOrganizationAge';
- import type {PromotionClaimed, Subscription} from 'getsentry/types';
- import {getProductTrial, getTrialDaysLeft} from './billing';
- // we encode sizes for bucketing using roygbiv coloring
- const SIZES = {
- NONE: 'red',
- XXSMALL: 'orange',
- XSMALL: 'yellow',
- SMALL: 'green',
- MEDIUM: 'blue',
- LARGE: 'indigo',
- XLARGE: 'violet',
- XXLARGE: 'teal',
- XXXLARGE: 'magenta',
- } as const;
- // a map of custom fields that rely on bucketing
- const CUSTOM_BUCKET_FIELDS = {
- arr: getReservedTotalFromSubscription,
- accountCredit: getAccountCredit,
- };
- type CustomFields = keyof typeof CUSTOM_BUCKET_FIELDS;
- type Size = (typeof SIZES)[keyof typeof SIZES];
- type BucketRecord = Array<[number, Size]>;
- type BucketMap = Partial<Record<keyof Subscription | CustomFields, BucketRecord>>;
- // the values in the bucket are the max values for each bucket inclusive
- // soo arr = 1,000 is small and 1,001 is medium
- const BUCKET_MAP: BucketMap = {
- totalMembers: [
- [1, SIZES.XXSMALL],
- [5, SIZES.XSMALL],
- [25, SIZES.SMALL],
- [100, SIZES.MEDIUM],
- [250, SIZES.LARGE],
- [500, SIZES.XLARGE],
- [1000, SIZES.XXLARGE],
- ],
- reservedErrors: [
- [5_000, SIZES.XXSMALL],
- [50_000, SIZES.XSMALL],
- [200_000, SIZES.SMALL],
- [1_000_000, SIZES.MEDIUM],
- [5_000_000, SIZES.LARGE],
- [10_000_000, SIZES.XLARGE],
- [15_000_000, SIZES.XXLARGE],
- ],
- reservedTransactions: [
- [10_000, SIZES.XXSMALL],
- [100_000, SIZES.XSMALL],
- [400_000, SIZES.SMALL],
- [2_000_000, SIZES.MEDIUM],
- [10_000_000, SIZES.LARGE],
- [20_000_000, SIZES.XLARGE],
- [50_000_000, SIZES.XXLARGE],
- ],
- arr: [
- [500, SIZES.XXSMALL],
- [1_000, SIZES.XSMALL],
- [5_000, SIZES.SMALL],
- [20_000, SIZES.MEDIUM],
- [50_000, SIZES.LARGE],
- [100_000_000, SIZES.XLARGE],
- [200_000_000, SIZES.XXLARGE],
- ],
- // $0 is red, <$5 is orange, $5-$29 is yellow, $29-$89 is green, $89-$348 is blue, $348-$1068 is indigo, $1068+ is violet
- accountCredit: [
- [4.99, SIZES.XXSMALL],
- [28.99, SIZES.XSMALL], // $29 (1 month team plan)
- [88.99, SIZES.SMALL], // $89 (1 month business plan)
- [347.99, SIZES.MEDIUM], // $348 (1 year team plan)
- [1067.99, SIZES.LARGE], // $1068 (1 year business plan)
- [2_000, SIZES.XLARGE],
- [4_000, SIZES.XXLARGE],
- ],
- };
- export function getPendoAccountFields(
- subscription: Subscription,
- organization: Organization,
- {
- activePromotions,
- completedPromotions,
- }: {
- activePromotions: PromotionClaimed[] | null;
- completedPromotions: PromotionClaimed[] | null;
- }
- ) {
- // add basic fields as-is
- const baseAccountFields = {
- ...pick(subscription, [
- 'isFree',
- 'isManaged',
- 'isTrial',
- 'isEnterpriseTrial',
- 'isPerformancePlanTrial',
- 'isSuspended',
- 'canTrial',
- 'canSelfServe',
- 'plan',
- 'planTier',
- ]),
- ...pick(organization, ['isEarlyAdopter']),
- };
- // for fields with bucketing, we need to encode the value so
- // we can obfuscate this information
- for (const field in BUCKET_MAP) {
- // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- const buckets = BUCKET_MAP[field];
- let value: number | undefined;
- if (field in subscription) {
- // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- value = subscription[field];
- } else if (field in CUSTOM_BUCKET_FIELDS) {
- // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- value = CUSTOM_BUCKET_FIELDS[field](subscription);
- }
- if (value !== undefined) {
- // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- baseAccountFields[field] = getBucketValue(value, buckets);
- }
- }
- const promoInfo: {
- activePromotion: null | string;
- completedPromotions: string;
- daysSincePromotionClaimed: number;
- freeEventCreditDaysLeft: number;
- isLastCycleForFreeEvents: boolean;
- promotionDaysLeft: number;
- } = {
- activePromotion: null,
- promotionDaysLeft: -1,
- completedPromotions: (completedPromotions || []).map(p => p.promotion.slug).join(','),
- freeEventCreditDaysLeft: -1,
- isLastCycleForFreeEvents: false,
- daysSincePromotionClaimed: -1,
- };
- if (activePromotions && activePromotions.length > 0) {
- const promo = activePromotions[0]!;
- promoInfo.activePromotion = promo.promotion.slug;
- promoInfo.daysSincePromotionClaimed = getDaysSinceDate(promo.dateClaimed);
- if (promo.promotion.endDate) {
- promoInfo.promotionDaysLeft = -1 * getDaysSinceDate(promo.promotion.endDate);
- }
- }
- if (completedPromotions && completedPromotions.length > 0) {
- const promo = completedPromotions[0]!;
- promoInfo.freeEventCreditDaysLeft = promo.freeEventCreditDaysLeft;
- promoInfo.isLastCycleForFreeEvents = promo.isLastCycleForFreeEvents;
- promoInfo.daysSincePromotionClaimed = getDaysSinceDate(promo.dateClaimed);
- }
- const perfTrial = getProductTrial(
- subscription.productTrials ?? null,
- DataCategory.TRANSACTIONS
- );
- const perfTrialAvailable: boolean = perfTrial ? !perfTrial.isStarted : false;
- const perfTrialStartDate: string = perfTrial?.startDate ?? '';
- const perfTrialEndDate: string = perfTrial?.endDate ?? '';
- const perfTrialActive: boolean = perfTrial
- ? perfTrial.isStarted && getDaysSinceDate(perfTrial.endDate ?? '') <= 0
- : false;
- const replayTrial = getProductTrial(
- subscription.productTrials ?? null,
- DataCategory.REPLAYS
- );
- const replayTrialAvailable: boolean = replayTrial ? !replayTrial.isStarted : false;
- const replayTrialStartDate: string = replayTrial?.startDate ?? '';
- const replayTrialEndDate: string = replayTrial?.endDate ?? '';
- const replayTrialActive: boolean = replayTrial
- ? replayTrial.isStarted && getDaysSinceDate(replayTrial.endDate ?? '') <= 0
- : false;
- const profilesTrial = getProductTrial(
- subscription.productTrials ?? null,
- DataCategory.PROFILE_DURATION
- );
- const profilesTrialAvailable: boolean = profilesTrial
- ? !profilesTrial.isStarted
- : false;
- const profilesTrialStartDate: string = profilesTrial?.startDate ?? '';
- const profilesTrialEndDate: string = profilesTrial?.endDate ?? '';
- const profilesTrialActive: boolean = profilesTrial
- ? profilesTrial.isStarted && getDaysSinceDate(profilesTrial.endDate ?? '') <= 0
- : false;
- const spansTrial = getProductTrial(
- subscription.productTrials ?? null,
- DataCategory.SPANS
- );
- const spansTrialAvailable: boolean = spansTrial ? !spansTrial.isStarted : false;
- const spansTrialStartDate: string = spansTrial?.startDate ?? '';
- const spansTrialEndDate: string = spansTrial?.endDate ?? '';
- const spansTrialActive: boolean = spansTrial
- ? spansTrial.isStarted && getDaysSinceDate(spansTrial.endDate ?? '') <= 0
- : false;
- return {
- ...baseAccountFields,
- trialDaysLeft: getTrialDaysLeftFromSub(subscription),
- organizationAge: getOrganizationAge(organization),
- // don't want to send in actual on-demand spend
- // so just send in a boolean flag if it's enabled
- // may want to convert to a bucketed field eventually
- hasOnDemandSpend: subscription.onDemandMaxSpend > 0,
- considerForDsUpsell: getConsiderForDsUpsell(subscription),
- perfTrialAvailable,
- perfTrialStartDate,
- perfTrialEndDate,
- perfTrialActive,
- replayTrialAvailable,
- replayTrialStartDate,
- replayTrialEndDate,
- replayTrialActive,
- profilesTrialAvailable,
- profilesTrialStartDate,
- profilesTrialEndDate,
- profilesTrialActive,
- spansTrialAvailable,
- spansTrialStartDate,
- spansTrialEndDate,
- spansTrialActive,
- ...promoInfo,
- };
- }
- function getBucketValue(value: number, buckets: BucketRecord): Size {
- // if 0, use special case
- if (value === 0) {
- return SIZES.NONE;
- }
- for (const bucket of buckets) {
- // if our value is less than or equal to the bucket, use it
- if (value <= bucket[0]) {
- return bucket[1];
- }
- }
- // if no match, use xxxlarge
- return SIZES.XXXLARGE;
- }
- /**
- * Get the value in dollars of the current subscription as the input and divide by 100
- */
- function getReservedTotalFromSubscription(subscription: Subscription) {
- const {planDetails} = subscription;
- const monthlyPrice =
- planDetails.billingInterval === 'annual'
- ? planDetails.totalPrice / 12
- : planDetails.totalPrice;
- return monthlyPrice / 100.0;
- }
- function getAccountCredit(subscription: Subscription) {
- // invert to get credit and divide by 100 to get $ amount
- return subscription.accountBalance / -100.0;
- }
- function getTrialDaysLeftFromSub(subscription: Subscription) {
- // only check if trial is active
- if (!subscription.isTrial) {
- return null;
- }
- return getTrialDaysLeft(subscription);
- }
- // consider the org if they have at least 5m reserved transactions
- function getConsiderForDsUpsell(subscription: Subscription) {
- return (subscription.reservedTransactions || 0) >= 5_000_000;
- }
|