123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 |
- import set from 'lodash/set';
- import {FieldObject} from 'sentry/components/forms/types';
- import {t} from 'sentry/locale';
- import {OrganizationSummary, Project} from 'sentry/types';
- import {
- ALL_PROVIDERS,
- MIN_PROJECTS_FOR_CONFIRMATION,
- NOTIFICATION_SETTINGS_PATHNAMES,
- NotificationSettingsByProviderObject,
- NotificationSettingsObject,
- VALUE_MAPPING,
- } from 'sentry/views/settings/account/notifications/constants';
- import {NOTIFICATION_SETTING_FIELDS} from 'sentry/views/settings/account/notifications/fields2';
- import ParentLabel from 'sentry/views/settings/account/notifications/parentLabel';
- /**
- * Which fine-tuning parts are grouped by project
- */
- const notificationsByProject = ['alerts', 'email', 'workflow', 'spikeProtection'];
- export const isGroupedByProject = (notificationType: string): boolean =>
- notificationsByProject.includes(notificationType);
- export const getParentKey = (notificationType: string): string => {
- return isGroupedByProject(notificationType) ? 'project' : 'organization';
- };
- export const groupByOrganization = (
- projects: Project[]
- ): Record<string, {organization: OrganizationSummary; projects: Project[]}> => {
- return projects.reduce<
- Record<string, {organization: OrganizationSummary; projects: Project[]}>
- >((acc, project) => {
- const orgSlug = project.organization.slug;
- if (acc.hasOwnProperty(orgSlug)) {
- acc[orgSlug].projects.push(project);
- } else {
- acc[orgSlug] = {
- organization: project.organization,
- projects: [project],
- };
- }
- return acc;
- }, {});
- };
- export const getFallBackValue = (notificationType: string): string => {
- switch (notificationType) {
- case 'alerts':
- return 'always';
- case 'deploy':
- return 'committed_only';
- case 'workflow':
- return 'subscribe_only';
- case 'approval':
- return 'always';
- case 'quota':
- return 'always';
- case 'spikeProtection':
- return 'always';
- case 'reports':
- return 'always';
- default:
- // These are the expected potential settings with fallback of ''
- // issue, quotaErrors, quotaTransactions, quotaAttachments,
- // quotaReplays, quotaWarnings, quotaSpendAllocations
- return '';
- }
- };
- export const providerListToString = (providers: string[]): string => {
- return providers.sort().join('+');
- };
- export const getChoiceString = (choices: string[][], key: string): string => {
- if (!choices) {
- return 'default';
- }
- const found = choices.find(row => row[0] === key);
- if (!found) {
- throw new Error(`Could not find ${key}`);
- }
- return found[1];
- };
- const isDataAllNever = (data: {[key: string]: string}): boolean =>
- !!Object.keys(data).length && Object.values(data).every(value => value === 'never');
- const getNonNeverValue = (data: {[key: string]: string}): string | null =>
- Object.values(data).reduce(
- (previousValue: string | null, currentValue) =>
- currentValue === 'never' ? previousValue : currentValue,
- null
- );
- /**
- * Transform `data`, a mapping of providers to values, so that all providers in
- * `providerList` are "on" in the resulting object. The "on" value is
- * determined by checking `data` for non-"never" values and falling back to the
- * value `fallbackValue`. The "off" value is either "default" or "never"
- * depending on whether `scopeType` is "parent" or "user" respectively.
- */
- export const backfillMissingProvidersWithFallback = (
- data: {[key: string]: string},
- providerList: string[],
- fallbackValue: string,
- scopeType: string
- ): NotificationSettingsByProviderObject => {
- // First pass: What was this scope's previous value?
- let existingValue;
- if (scopeType === 'user') {
- existingValue = isDataAllNever(data)
- ? fallbackValue
- : getNonNeverValue(data) || fallbackValue;
- } else {
- existingValue = isDataAllNever(data) ? 'never' : getNonNeverValue(data) || 'default';
- }
- // Second pass: Fill in values for every provider.
- return Object.fromEntries(
- Object.keys(ALL_PROVIDERS).map(provider => [
- provider,
- providerList.includes(provider) ? existingValue : 'never',
- ])
- );
- };
- /**
- * Deeply merge N notification settings objects (usually just 2).
- */
- export const mergeNotificationSettings = (
- ...objects: NotificationSettingsObject[]
- ): NotificationSettingsObject => {
- const output: NotificationSettingsObject = {};
- objects.forEach(settingsByType =>
- Object.entries(settingsByType).forEach(([type, settingsByScopeType]) =>
- Object.entries(settingsByScopeType).forEach(([scopeType, settingsByScopeId]) =>
- Object.entries(settingsByScopeId).forEach(([scopeId, settingsByProvider]) => {
- set(output, [type, scopeType, scopeId].join('.'), settingsByProvider);
- })
- )
- )
- );
- return output;
- };
- /**
- * Get the mapping of providers to values that describe a user's parent-
- * independent notification preferences. The data from the API uses the user ID
- * rather than "me" so we assume the first ID is the user's.
- */
- export const getUserDefaultValues = (
- notificationType: string,
- notificationSettings: NotificationSettingsObject
- ): NotificationSettingsByProviderObject => {
- return (
- Object.values(notificationSettings[notificationType]?.user || {}).pop() ||
- Object.fromEntries(
- Object.entries(ALL_PROVIDERS).map(([provider, value]) => [
- provider,
- value === 'default' ? getFallBackValue(notificationType) : value,
- ])
- )
- );
- };
- /**
- * Get the list of providers currently active on this page. Note: this can be empty.
- */
- export const getCurrentProviders = (
- notificationType: string,
- notificationSettings: NotificationSettingsObject
- ): string[] => {
- const userData = getUserDefaultValues(notificationType, notificationSettings);
- return Object.entries(userData)
- .filter(([_, value]) => !['never'].includes(value))
- .map(([provider, _]) => provider);
- };
- /**
- * Calculate the currently selected provider.
- */
- export const getCurrentDefault = (
- notificationType: string,
- notificationSettings: NotificationSettingsObject
- ): string => {
- const providersList = getCurrentProviders(notificationType, notificationSettings);
- return providersList.length
- ? getUserDefaultValues(notificationType, notificationSettings)[providersList[0]]
- : 'never';
- };
- /**
- * For a given notificationType, are the parent-independent setting "never" for
- * all providers and are the parent-specific settings "default" or "never". If
- * so, the API is telling us that the user has opted out of all notifications.
- */
- export const decideDefault = (
- notificationType: string,
- notificationSettings: NotificationSettingsObject
- ): string => {
- const compare = (a: string, b: string): number => VALUE_MAPPING[a] - VALUE_MAPPING[b];
- const parentIndependentSetting =
- Object.values(getUserDefaultValues(notificationType, notificationSettings))
- .sort(compare)
- .pop() || 'never';
- if (parentIndependentSetting !== 'never') {
- return parentIndependentSetting;
- }
- const parentSpecificSetting =
- Object.values(
- notificationSettings[notificationType]?.[getParentKey(notificationType)] || {}
- )
- .flatMap(settingsByProvider => Object.values(settingsByProvider))
- .sort(compare)
- .pop() || 'default';
- return parentSpecificSetting === 'default' ? 'never' : parentSpecificSetting;
- };
- /**
- * For a given notificationType, are the parent-independent setting "never" for
- * all providers and are the parent-specific settings "default" or "never"? If
- * so, the API is telling us that the user has opted out of all notifications.
- */
- export const isEverythingDisabled = (
- notificationType: string,
- notificationSettings: NotificationSettingsObject
- ): boolean =>
- ['never', 'default'].includes(decideDefault(notificationType, notificationSettings));
- /**
- * Extract either the list of project or organization IDs from the notification
- * settings in state. This assumes that the notification settings object is
- * fully backfilled with settings for every parent.
- */
- export const getParentIds = (
- notificationType: string,
- notificationSettings: NotificationSettingsObject
- ): string[] =>
- Object.keys(
- notificationSettings[notificationType]?.[getParentKey(notificationType)] || {}
- );
- export const getParentValues = (
- notificationType: string,
- notificationSettings: NotificationSettingsObject,
- parentId: string
- ): NotificationSettingsByProviderObject =>
- notificationSettings[notificationType]?.[getParentKey(notificationType)]?.[
- parentId
- ] || {
- email: 'default',
- };
- /**
- * Get a mapping of all parent IDs to the notification setting for the current
- * providers.
- */
- export const getParentData = (
- notificationType: string,
- notificationSettings: NotificationSettingsObject,
- parents: OrganizationSummary[] | Project[]
- ): NotificationSettingsByProviderObject => {
- const provider = getCurrentProviders(notificationType, notificationSettings)[0];
- return Object.fromEntries(
- parents.map(parent => [
- parent.id,
- getParentValues(notificationType, notificationSettings, parent.id)[provider],
- ])
- );
- };
- /**
- * Are there are more than N project or organization settings?
- */
- export const isSufficientlyComplex = (
- notificationType: string,
- notificationSettings: NotificationSettingsObject
- ): boolean =>
- getParentIds(notificationType, notificationSettings).length >
- MIN_PROJECTS_FOR_CONFIRMATION;
- /**
- * This is triggered when we change the Delivery Method select. Don't update the
- * provider for EVERY one of the user's projects and organizations, just the user
- * and parents that have explicit settings.
- */
- export const getStateToPutForProvider = (
- notificationType: string,
- notificationSettings: NotificationSettingsObject,
- changedData: NotificationSettingsByProviderObject
- ): NotificationSettingsObject => {
- const providerList: string[] = changedData.provider
- ? Object.values(changedData.provider)
- : [];
- const fallbackValue = getFallBackValue(notificationType);
- // If the user has no settings, we need to create them.
- if (!Object.keys(notificationSettings).length) {
- return {
- [notificationType]: {
- user: {
- me: Object.fromEntries(providerList.map(provider => [provider, fallbackValue])),
- },
- },
- };
- }
- return {
- [notificationType]: Object.fromEntries(
- Object.entries(notificationSettings[notificationType]).map(
- ([scopeType, scopeTypeData]) => [
- scopeType,
- Object.fromEntries(
- Object.entries(scopeTypeData).map(([scopeId, scopeIdData]) => [
- scopeId,
- backfillMissingProvidersWithFallback(
- scopeIdData,
- providerList,
- fallbackValue,
- scopeType
- ),
- ])
- ),
- ]
- )
- ),
- };
- };
- /**
- * Update the current providers' parent-independent notification settings with
- * the new value. If the new value is "never", then also update all
- * parent-specific notification settings to "default". If the previous value
- * was "never", then assume providerList should be "email" only.
- */
- export const getStateToPutForDefault = (
- notificationType: string,
- notificationSettings: NotificationSettingsObject,
- changedData: NotificationSettingsByProviderObject,
- parentIds: string[]
- ): NotificationSettingsObject => {
- const newValue = Object.values(changedData)[0];
- let providerList = getCurrentProviders(notificationType, notificationSettings);
- if (!providerList.length) {
- providerList = ['email'];
- }
- const updatedNotificationSettings = {
- [notificationType]: {
- user: {
- me: Object.fromEntries(providerList.map(provider => [provider, newValue])),
- },
- },
- };
- if (newValue === 'never') {
- updatedNotificationSettings[notificationType][getParentKey(notificationType)] =
- Object.fromEntries(
- parentIds.map(parentId => [
- parentId,
- Object.fromEntries(providerList.map(provider => [provider, 'default'])),
- ])
- );
- }
- return updatedNotificationSettings;
- };
- /**
- * Get the diff of the Notification Settings for this parent ID.
- */
- export const getStateToPutForParent = (
- notificationType: string,
- notificationSettings: NotificationSettingsObject,
- changedData: NotificationSettingsByProviderObject,
- parentId: string
- ): NotificationSettingsObject => {
- const providerList = getCurrentProviders(notificationType, notificationSettings);
- const newValue = Object.values(changedData)[0];
- return {
- [notificationType]: {
- [getParentKey(notificationType)]: {
- [parentId]: Object.fromEntries(
- providerList.map(provider => [provider, newValue])
- ),
- },
- },
- };
- };
- /**
- * Render each parent and add a default option to the the field choices.
- */
- export const getParentField = (
- notificationType: string,
- notificationSettings: NotificationSettingsObject,
- parent: OrganizationSummary | Project,
- onChange: (
- changedData: NotificationSettingsByProviderObject,
- parentId: string
- ) => NotificationSettingsObject
- ): FieldObject => {
- const defaultFields = NOTIFICATION_SETTING_FIELDS[notificationType];
- let choices = defaultFields.choices;
- if (Array.isArray(choices)) {
- choices = choices.concat([
- [
- 'default',
- `${t('Default')} (${getChoiceString(
- choices,
- getCurrentDefault(notificationType, notificationSettings)
- )})`,
- ],
- ]);
- }
- return Object.assign({}, defaultFields, {
- label: <ParentLabel parent={parent} notificationType={notificationType} />,
- getData: data => onChange(data, parent.id),
- name: parent.id,
- choices,
- defaultValue: 'default',
- help: undefined,
- }) as any;
- };
- /**
- * Returns a link to docs on explaining how to manage quotas for that event type
- */
- export function getDocsLinkForEventType(
- event: 'error' | 'transaction' | 'attachment' | 'replay'
- ) {
- switch (event) {
- case 'transaction':
- return 'https://docs.sentry.io/product/performance/transaction-summary/#what-is-a-transaction';
- case 'attachment':
- return 'https://docs.sentry.io/product/accounts/quotas/manage-attachments-quota/#2-rate-limiting';
- case 'replay':
- return 'https://docs.sentry.io/product/session-replay/';
- default:
- return 'https://docs.sentry.io/product/accounts/quotas/manage-event-stream-guide/#common-workflows-for-managing-your-event-stream';
- }
- }
- /**
- * Returns the corresponding notification type name from the router path name
- */
- export function getNotificationTypeFromPathname(routerPathname: string) {
- const result = Object.entries(NOTIFICATION_SETTINGS_PATHNAMES).find(
- ([_, pathname]) => pathname === routerPathname
- ) ?? [routerPathname];
- return result[0];
- }
|