withExperiment.tsx 3.5 KB

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