|
1 | | -import {t} from 'sentry/locale'; |
| 1 | +import {Fragment} from 'react'; |
| 2 | +import upperFirst from 'lodash/upperFirst'; |
| 3 | + |
| 4 | +import {ExternalLink} from '@sentry/scraps/link'; |
| 5 | + |
| 6 | +import type {Field} from 'sentry/components/forms/types'; |
| 7 | +import QuestionTooltip from 'sentry/components/questionTooltip'; |
| 8 | +import {DATA_CATEGORY_INFO} from 'sentry/constants'; |
| 9 | +import {t, tct} from 'sentry/locale'; |
2 | 10 | import type {SelectValue} from 'sentry/types/core'; |
| 11 | +import {DataCategoryExact} from 'sentry/types/core'; |
| 12 | +import {getPricingDocsLinkForEventType} from 'sentry/views/settings/account/notifications/utils'; |
3 | 13 |
|
4 | 14 | export type FineTuneField = { |
5 | 15 | description: string; |
@@ -108,3 +118,232 @@ export const ACCOUNT_NOTIFICATION_FIELDS: Record<string, FineTuneField> = { |
108 | 118 | // Component will create choices |
109 | 119 | }, |
110 | 120 | }; |
| 121 | + |
| 122 | +export const NOTIFICATION_SETTING_FIELDS = { |
| 123 | + alerts: { |
| 124 | + name: 'alerts', |
| 125 | + type: 'select', |
| 126 | + label: t('Issue Alerts'), |
| 127 | + choices: [ |
| 128 | + ['always', t('On')], |
| 129 | + ['never', t('Off')], |
| 130 | + ], |
| 131 | + help: t('Notifications sent from Alert rules that your team has set up.'), |
| 132 | + }, |
| 133 | + workflow: { |
| 134 | + name: 'workflow', |
| 135 | + type: 'select', |
| 136 | + label: t('Issue Workflow'), |
| 137 | + choices: [ |
| 138 | + ['always', t('On')], |
| 139 | + ['subscribe_only', t('Only Subscribed Issues')], |
| 140 | + ['never', t('Off')], |
| 141 | + ], |
| 142 | + help: t('Changes in issue assignment, resolution status, and comments.'), |
| 143 | + }, |
| 144 | + deploy: { |
| 145 | + name: 'deploy', |
| 146 | + type: 'select', |
| 147 | + label: t('Deploys'), |
| 148 | + choices: [ |
| 149 | + ['always', t('On')], |
| 150 | + ['committed_only', t('Releases with My Commits')], |
| 151 | + ['never', t('Off')], |
| 152 | + ], |
| 153 | + help: t('Release, environment, and commit overviews.'), |
| 154 | + }, |
| 155 | + provider: { |
| 156 | + name: 'provider', |
| 157 | + type: 'select', |
| 158 | + label: t('Delivery Method'), |
| 159 | + choices: [ |
| 160 | + ['email', t('Email')], |
| 161 | + ['slack', t('Slack')], |
| 162 | + ['msteams', t('Microsoft Teams')], |
| 163 | + ], |
| 164 | + help: t('Where personal notifications will be sent.'), |
| 165 | + multiple: true, |
| 166 | + onChange: val => { |
| 167 | + // This is a little hack to prevent this field from being empty. |
| 168 | + // TODO(nisanthan): need to prevent showing the clearable on. the multi-select when its only 1 value. |
| 169 | + if (!val || val.length === 0) { |
| 170 | + throw new Error('Invalid selection. Field cannot be empty.'); |
| 171 | + } |
| 172 | + }, |
| 173 | + }, |
| 174 | + approval: { |
| 175 | + name: 'approval', |
| 176 | + type: 'select', |
| 177 | + label: t('Nudges'), |
| 178 | + choices: [ |
| 179 | + ['always', t('On')], |
| 180 | + ['never', t('Off')], |
| 181 | + ], |
| 182 | + help: t('Notifications that require review or approval.'), |
| 183 | + }, |
| 184 | + quota: { |
| 185 | + name: 'quota', |
| 186 | + type: 'select', |
| 187 | + label: t('Quota'), |
| 188 | + choices: [ |
| 189 | + ['always', t('On')], |
| 190 | + ['never', t('Off')], |
| 191 | + ], |
| 192 | + help: t('Error, transaction, replay, attachment, and cron monitor quota limits.'), |
| 193 | + }, |
| 194 | + reports: { |
| 195 | + name: 'reports', |
| 196 | + type: 'select', |
| 197 | + label: t('Weekly Reports'), |
| 198 | + help: t('A summary of the past week for an organization.'), |
| 199 | + choices: [ |
| 200 | + ['always', t('On')], |
| 201 | + ['never', t('Off')], |
| 202 | + ], |
| 203 | + }, |
| 204 | + email: { |
| 205 | + name: 'email routing', |
| 206 | + type: 'blank', |
| 207 | + choices: undefined, |
| 208 | + label: t('Email Routing'), |
| 209 | + help: t('Change the email address that receives notifications.'), |
| 210 | + }, |
| 211 | + spikeProtection: { |
| 212 | + name: 'spikeProtection', |
| 213 | + type: 'select', |
| 214 | + label: t('Spike Protection'), |
| 215 | + choices: [ |
| 216 | + ['always', t('On')], |
| 217 | + ['never', t('Off')], |
| 218 | + ], |
| 219 | + help: t('Notifications about spikes on a per project basis.'), |
| 220 | + }, |
| 221 | + brokenMonitors: { |
| 222 | + name: 'brokenMonitors', |
| 223 | + type: 'select', |
| 224 | + label: t('Broken Cron Monitors'), |
| 225 | + choices: [ |
| 226 | + ['always', t('On')], |
| 227 | + ['never', t('Off')], |
| 228 | + ], |
| 229 | + help: t( |
| 230 | + 'Notifications for Cron Monitors that have been in a failing state for a prolonged period of time' |
| 231 | + ), |
| 232 | + }, |
| 233 | + // legacy options |
| 234 | + personalActivityNotifications: { |
| 235 | + name: 'personalActivityNotifications', |
| 236 | + type: 'select', |
| 237 | + label: t('My Own Activity'), |
| 238 | + choices: [ |
| 239 | + [true as any, t('On')], |
| 240 | + [false as any, t('Off')], |
| 241 | + ], |
| 242 | + help: t('Notifications about your own actions on Sentry.'), |
| 243 | + }, |
| 244 | + selfAssignOnResolve: { |
| 245 | + name: 'selfAssignOnResolve', |
| 246 | + type: 'select', |
| 247 | + label: t('Resolve and Auto-Assign'), |
| 248 | + choices: [ |
| 249 | + [true as any, t('On')], |
| 250 | + [false as any, t('Off')], |
| 251 | + ], |
| 252 | + help: t("When you resolve an unassigned issue, we'll auto-assign it to you."), |
| 253 | + }, |
| 254 | +} satisfies Record<string, Field>; |
| 255 | + |
| 256 | +const CATEGORY_QUOTA_FIELDS = Object.values(DATA_CATEGORY_INFO) |
| 257 | + .filter( |
| 258 | + categoryInfo => |
| 259 | + categoryInfo.isBilledCategory && |
| 260 | + // Exclude Seer categories as they will be handled by a combined quotaSeerBudget field |
| 261 | + categoryInfo.name !== DataCategoryExact.SEER_AUTOFIX && |
| 262 | + categoryInfo.name !== DataCategoryExact.SEER_SCANNER |
| 263 | + ) |
| 264 | + .map(categoryInfo => { |
| 265 | + return { |
| 266 | + name: 'quota' + upperFirst(categoryInfo.plural), |
| 267 | + label: categoryInfo.titleName, |
| 268 | + help: tct( |
| 269 | + `Receive notifications about your [displayName] quotas. [learnMore:Learn more]`, |
| 270 | + { |
| 271 | + displayName: categoryInfo.displayName, |
| 272 | + learnMore: ( |
| 273 | + <ExternalLink href={getPricingDocsLinkForEventType(categoryInfo.name)} /> |
| 274 | + ), |
| 275 | + } |
| 276 | + ), |
| 277 | + choices: [ |
| 278 | + ['always', t('On')], |
| 279 | + ['never', t('Off')], |
| 280 | + ] as const, |
| 281 | + }; |
| 282 | + }); |
| 283 | + |
| 284 | +// Define the combined Seer budget field |
| 285 | +const quotaSeerBudgetField = { |
| 286 | + // This maps to NotificationSettingEnum.QUOTA_SEER_BUDGET |
| 287 | + name: 'quotaSeerBudget', |
| 288 | + label: t('Seer Budget'), |
| 289 | + help: tct(`Receive notifications for your Seer budget. [learnMore:Learn more]`, { |
| 290 | + learnMore: ( |
| 291 | + <ExternalLink |
| 292 | + href={getPricingDocsLinkForEventType(DataCategoryExact.SEER_AUTOFIX)} |
| 293 | + /> |
| 294 | + ), |
| 295 | + }), |
| 296 | + choices: [ |
| 297 | + ['always', t('On')], |
| 298 | + ['never', t('Off')], |
| 299 | + ] as const, |
| 300 | +}; |
| 301 | + |
| 302 | +// partial field definition for quota sub-categories |
| 303 | +export const QUOTA_FIELDS = [ |
| 304 | + { |
| 305 | + name: 'quotaWarnings', |
| 306 | + label: t('Set Quota Limit'), |
| 307 | + help: t('Receive notifications when your organization exceeds the following limits.'), |
| 308 | + choices: [ |
| 309 | + ['always', t('100% and 80%')], |
| 310 | + ['never', t('100%')], |
| 311 | + ] as const, |
| 312 | + }, |
| 313 | + ...CATEGORY_QUOTA_FIELDS, |
| 314 | + quotaSeerBudgetField, |
| 315 | + { |
| 316 | + name: 'quotaSpendAllocations', |
| 317 | + label: ( |
| 318 | + <Fragment> |
| 319 | + {t('Spend Allocations')}{' '} |
| 320 | + <QuestionTooltip position="top" title="Business plan only" size="xs" /> |
| 321 | + </Fragment> |
| 322 | + ), |
| 323 | + help: t('Receive notifications about your spend allocations.'), |
| 324 | + choices: [ |
| 325 | + ['always', t('On')], |
| 326 | + ['never', t('Off')], |
| 327 | + ] as const, |
| 328 | + }, |
| 329 | +]; |
| 330 | + |
| 331 | +export const SPEND_FIELDS = [ |
| 332 | + { |
| 333 | + name: 'quota', |
| 334 | + label: t('Spend Notifications'), |
| 335 | + help: tct( |
| 336 | + 'Receive notifications when your spend crosses predefined or custom thresholds. [learnMore:Learn more]', |
| 337 | + { |
| 338 | + learnMore: ( |
| 339 | + <ExternalLink href="https://docs.sentry.io/product/alerts/notifications/#spend-notifications" /> |
| 340 | + ), |
| 341 | + } |
| 342 | + ), |
| 343 | + choices: [ |
| 344 | + ['always', t('On')], |
| 345 | + ['never', t('Off')], |
| 346 | + ] as const, |
| 347 | + }, |
| 348 | + ...QUOTA_FIELDS.slice(1), |
| 349 | +]; |
0 commit comments