integrationUtil.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import capitalize from 'lodash/capitalize';
  2. import * as qs from 'query-string';
  3. import {
  4. IconBitbucket,
  5. IconGeneric,
  6. IconGithub,
  7. IconGitlab,
  8. IconJira,
  9. IconVsts,
  10. } from 'app/icons';
  11. import HookStore from 'app/stores/hookStore';
  12. import {
  13. AppOrProviderOrPlugin,
  14. DocumentIntegration,
  15. IntegrationFeature,
  16. IntegrationInstallationStatus,
  17. IntegrationType,
  18. Organization,
  19. PluginWithProjectList,
  20. SentryApp,
  21. SentryAppInstallation,
  22. } from 'app/types';
  23. import {Hooks} from 'app/types/hooks';
  24. import {EventParameters, trackAdvancedAnalyticsEvent} from 'app/utils/advancedAnalytics';
  25. import {IntegrationAnalyticsKey} from 'app/utils/integrationEvents';
  26. const mapIntegrationParams = analyticsParams => {
  27. // Reload expects integration_status even though it's not relevant for non-sentry apps
  28. // Passing in a dummy value of published in those cases
  29. const fullParams = {...analyticsParams};
  30. if (analyticsParams.integration && analyticsParams.integration_type !== 'sentry_app') {
  31. fullParams.integration_status = 'published';
  32. }
  33. return fullParams;
  34. };
  35. // wrapper around trackAdvancedAnalyticsEvent which has some extra
  36. // data massaging above
  37. export function trackIntegrationEvent<T extends IntegrationAnalyticsKey>(
  38. eventKey: T,
  39. analyticsParams: EventParameters[T],
  40. org: Organization, // integration events should always be tied to an org
  41. options?: Parameters<typeof trackAdvancedAnalyticsEvent>[3]
  42. ) {
  43. return trackAdvancedAnalyticsEvent(
  44. eventKey,
  45. analyticsParams,
  46. org,
  47. options,
  48. mapIntegrationParams
  49. );
  50. }
  51. /**
  52. * In sentry.io the features list supports rendering plan details. If the hook
  53. * is not registered for rendering the features list like this simply show the
  54. * features as a normal list.
  55. */
  56. const generateFeaturesList = p => (
  57. <ul>
  58. {p.features.map((f, i) => (
  59. <li key={i}>{f.description}</li>
  60. ))}
  61. </ul>
  62. );
  63. const generateIntegrationFeatures = p =>
  64. p.children({
  65. disabled: false,
  66. disabledReason: null,
  67. ungatedFeatures: p.features,
  68. gatedFeatureGroups: [],
  69. });
  70. const defaultFeatureGateComponents = {
  71. IntegrationFeatures: generateIntegrationFeatures,
  72. IntegrationDirectoryFeatures: generateIntegrationFeatures,
  73. FeatureList: generateFeaturesList,
  74. IntegrationDirectoryFeatureList: generateFeaturesList,
  75. } as ReturnType<Hooks['integrations:feature-gates']>;
  76. export const getIntegrationFeatureGate = () => {
  77. const defaultHook = () => defaultFeatureGateComponents;
  78. const featureHook = HookStore.get('integrations:feature-gates')[0] || defaultHook;
  79. return featureHook();
  80. };
  81. export const getSentryAppInstallStatus = (install: SentryAppInstallation | undefined) => {
  82. if (install) {
  83. return capitalize(install.status) as IntegrationInstallationStatus;
  84. }
  85. return 'Not Installed';
  86. };
  87. export const getCategories = (features: IntegrationFeature[]): string[] => {
  88. const transform = features.map(({featureGate}) => {
  89. const feature = featureGate
  90. .replace(/integrations/g, '')
  91. .replace(/-/g, ' ')
  92. .trim();
  93. switch (feature) {
  94. case 'actionable notification':
  95. return 'notification action';
  96. case 'issue basic':
  97. case 'issue link':
  98. case 'issue sync':
  99. return 'project management';
  100. case 'commits':
  101. return 'source code management';
  102. case 'chat unfurl':
  103. return 'chat';
  104. default:
  105. return feature;
  106. }
  107. });
  108. return [...new Set(transform)];
  109. };
  110. export const getCategoriesForIntegration = (
  111. integration: AppOrProviderOrPlugin
  112. ): string[] => {
  113. if (isSentryApp(integration)) {
  114. return ['internal', 'unpublished'].includes(integration.status)
  115. ? [integration.status]
  116. : getCategories(integration.featureData);
  117. }
  118. if (isPlugin(integration)) {
  119. return getCategories(integration.featureDescriptions);
  120. }
  121. if (isDocumentIntegration(integration)) {
  122. return getCategories(integration.features);
  123. }
  124. return getCategories(integration.metadata.features);
  125. };
  126. export function isSentryApp(
  127. integration: AppOrProviderOrPlugin
  128. ): integration is SentryApp {
  129. return !!(integration as SentryApp).uuid;
  130. }
  131. export function isPlugin(
  132. integration: AppOrProviderOrPlugin
  133. ): integration is PluginWithProjectList {
  134. return integration.hasOwnProperty('shortName');
  135. }
  136. export function isDocumentIntegration(
  137. integration: AppOrProviderOrPlugin
  138. ): integration is DocumentIntegration {
  139. return integration.hasOwnProperty('docUrl');
  140. }
  141. export const getIntegrationType = (
  142. integration: AppOrProviderOrPlugin
  143. ): IntegrationType => {
  144. if (isSentryApp(integration)) {
  145. return 'sentry_app';
  146. }
  147. if (isPlugin(integration)) {
  148. return 'plugin';
  149. }
  150. if (isDocumentIntegration(integration)) {
  151. return 'document';
  152. }
  153. return 'first_party';
  154. };
  155. export const convertIntegrationTypeToSnakeCase = (
  156. type: 'plugin' | 'firstParty' | 'sentryApp' | 'documentIntegration'
  157. ) => {
  158. switch (type) {
  159. case 'firstParty':
  160. return 'first_party';
  161. case 'sentryApp':
  162. return 'sentry_app';
  163. case 'documentIntegration':
  164. return 'document';
  165. default:
  166. return type;
  167. }
  168. };
  169. export const safeGetQsParam = (param: string) => {
  170. try {
  171. const query = qs.parse(window.location.search) || {};
  172. return query[param];
  173. } catch {
  174. return undefined;
  175. }
  176. };
  177. export const getIntegrationIcon = (integrationType?: string, size?: string) => {
  178. const iconSize = size || 'md';
  179. switch (integrationType) {
  180. case 'bitbucket':
  181. return <IconBitbucket size={iconSize} />;
  182. case 'gitlab':
  183. return <IconGitlab size={iconSize} />;
  184. case 'github':
  185. case 'github_enterprise':
  186. return <IconGithub size={iconSize} />;
  187. case 'jira':
  188. case 'jira_server':
  189. return <IconJira size={iconSize} />;
  190. case 'vsts':
  191. return <IconVsts size={iconSize} />;
  192. default:
  193. return <IconGeneric size={iconSize} />;
  194. }
  195. };
  196. // used for project creation and onboarding
  197. // determines what integration maps to what project platform
  198. export const platfromToIntegrationMap = {
  199. 'node-awslambda': 'aws_lambda',
  200. 'python-awslambda': 'aws_lambda',
  201. };