withExperiment.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import * as React from 'react';
  2. import {experimentConfig, unassignedValue} from 'sentry/data/experimentConfig';
  3. import ConfigStore from 'sentry/stores/configStore';
  4. import {Organization} from 'sentry/types';
  5. import {
  6. ExperimentAssignment,
  7. ExperimentKey,
  8. Experiments,
  9. ExperimentType,
  10. OrgExperiments,
  11. UserExperiments,
  12. } from 'sentry/types/experiments';
  13. import {logExperiment} from 'sentry/utils/analytics';
  14. import getDisplayName from 'sentry/utils/getDisplayName';
  15. type Options<E extends ExperimentKey, L extends boolean> = {
  16. /**
  17. * The key of the experiment that will be injected into the component
  18. */
  19. experiment: E;
  20. /**
  21. * By default this HoC will log the exposure of the experiment upon mounting
  22. * of the component.
  23. *
  24. * If this is undesirable, for example if the experiment is hidden behind
  25. * some user action beyond this component being mounted, then you will want
  26. * to customize when exposure to the experiment has been logged.
  27. *
  28. * Marking this value as true will inject a `logExperiment` function as a
  29. * prop which takes no parameters and will log exposure of the experiment
  30. * when called.
  31. *
  32. * NOTE: If set to true, YOU ARE RESPONSIBLE for logging exposure of the
  33. * experiment!! If you do not log exposure your experiment will not be
  34. * correct!!
  35. */
  36. injectLogExperiment?: L;
  37. };
  38. type ExpectedProps<T extends ExperimentType> = T extends 'organization'
  39. ? {organization: Organization}
  40. : {};
  41. type InjectedExperimentProps<E extends ExperimentKey, L extends boolean> = {
  42. /**
  43. * The value of the injected experiment. Use this to determine behavior of
  44. * your component depending on the value.
  45. */
  46. experimentAssignment: ExperimentAssignment[E];
  47. } & (L extends true ? LogExperimentProps : {});
  48. type LogExperimentProps = {
  49. /**
  50. * Call this method when the user has been exposed to the experiment this
  51. * component has been provided the value of.
  52. */
  53. logExperiment: () => void;
  54. };
  55. /**
  56. * A HoC wrapper that injects `experimentAssignment` into a component
  57. *
  58. * This wrapper will automatically log exposure of the experiment upon
  59. * receiving the componentDidMount lifecycle event.
  60. *
  61. * For organization experiments, an organization object must be provided to the
  62. * component. You may wish to use the withOrganization HoC for this.
  63. *
  64. * If exposure logging upon mount is not desirable, The `injectLogExperiment`
  65. * option may be of use.
  66. *
  67. * NOTE: When using this you will have to type the `experimentAssignment` prop
  68. * on your component. For this you should use the `ExperimentAssignment`
  69. * mapped type.
  70. */
  71. function withExperiment<
  72. E extends ExperimentKey,
  73. L extends boolean,
  74. P extends InjectedExperimentProps<E, L>
  75. >(Component: React.ComponentType<P>, {experiment, injectLogExperiment}: Options<E, L>) {
  76. type Props = Omit<P, keyof InjectedExperimentProps<E, L>> &
  77. ExpectedProps<Experiments[E]['type']>;
  78. return class extends React.Component<Props> {
  79. static displayName = `withExperiment[${experiment}](${getDisplayName(Component)})`;
  80. // NOTE(ts): Because of the type complexity of this HoC, typescript
  81. // has a hard time understanding how to narrow Experiments[E]['type']
  82. // when we type assert on it.
  83. //
  84. // This means we have to do some typecasting to massage things into working
  85. // as expected.
  86. //
  87. // We DO guarantee the external API of this HoC is typed accurately.
  88. componentDidMount() {
  89. if (!injectLogExperiment) {
  90. this.logExperiment();
  91. }
  92. }
  93. getProps<Q extends ExperimentType>() {
  94. return this.props as unknown as ExpectedProps<Q>;
  95. }
  96. get config() {
  97. return experimentConfig[experiment];
  98. }
  99. get experimentAssignment() {
  100. const {type} = this.config;
  101. if (type === ExperimentType.Organization) {
  102. const key = experiment as keyof OrgExperiments;
  103. return this.getProps<typeof type>().organization.experiments[key];
  104. }
  105. if (type === ExperimentType.User) {
  106. const key = experiment as keyof UserExperiments;
  107. return ConfigStore.get('user').experiments[key];
  108. }
  109. return unassignedValue;
  110. }
  111. logExperiment = () =>
  112. logExperiment({
  113. key: experiment,
  114. organization: this.getProps<ExperimentType.Organization>().organization,
  115. });
  116. render() {
  117. const WrappedComponent = Component as React.JSXElementConstructor<any>;
  118. const props = {
  119. experimentAssignment: this.experimentAssignment,
  120. ...(injectLogExperiment ? {logExperiment: this.logExperiment} : {}),
  121. ...this.props,
  122. } as unknown;
  123. return <WrappedComponent {...(props as P)} />;
  124. }
  125. };
  126. }
  127. export default withExperiment;