featureFlagOverrides.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import type {Organization} from 'sentry/types/organization';
  2. import localStorageWrapper from 'sentry/utils/localStorage';
  3. type OverrideState = Record<string, boolean>;
  4. // TODO(ryan953): this should import from the devtoolbar definition
  5. type FlagValue = boolean | string | number | undefined;
  6. export type FeatureFlagMap = Record<string, {override: FlagValue; value: FlagValue}>;
  7. const LOCALSTORAGE_KEY = 'feature-flag-overrides';
  8. let __SINGLETON: FeatureFlagOverrides | null = null;
  9. export default class FeatureFlagOverrides {
  10. /**
  11. * Return the same instance of FeatureFlagOverrides in each part of the app.
  12. *
  13. * Multiple instances of FeatureFlagOverrides are needed by tests only.
  14. */
  15. public static singleton() {
  16. if (!__SINGLETON) {
  17. __SINGLETON = new FeatureFlagOverrides();
  18. }
  19. return __SINGLETON;
  20. }
  21. /**
  22. * Instead of storing the original & overridden values on the org itself we're
  23. * using this cache instead. Having the cache on the side means we don't need
  24. * to change the Organization type to add a pr
  25. */
  26. private _originalValues = new WeakMap<Organization, FeatureFlagMap>();
  27. /**
  28. * Set an override value into localStorage, so that the next time the page
  29. * loads we can read it and apply it to the org.
  30. */
  31. public setStoredOverride(name: string, value: boolean): void {
  32. try {
  33. const prev = this._getStoredOverrides();
  34. const updated: OverrideState = {...prev, [name]: value};
  35. localStorageWrapper.setItem(LOCALSTORAGE_KEY, JSON.stringify(updated));
  36. } catch {
  37. //
  38. }
  39. }
  40. public clear(): void {
  41. localStorageWrapper.setItem(LOCALSTORAGE_KEY, '{}');
  42. }
  43. private _getStoredOverrides(): OverrideState {
  44. try {
  45. return JSON.parse(localStorageWrapper.getItem(LOCALSTORAGE_KEY) ?? '{}');
  46. } catch {
  47. return {};
  48. }
  49. }
  50. /**
  51. * Convert the list of enabled org-features into a FeatureFlapMap and cache it
  52. * This cached list is only the original values that the server told us, but
  53. * in a format we can add overrides to later.
  54. */
  55. private _getNonOverriddenFeatures(organization: Organization): FeatureFlagMap {
  56. if (this._originalValues.has(organization)) {
  57. // @ts-expect-error: We just checked .has(), so it shouldn't be undefined
  58. return this._originalValues.get(organization);
  59. }
  60. const nonOverriddenFeatures = Object.fromEntries(
  61. organization.features.map(name => [name, {value: true, override: undefined}])
  62. );
  63. this._originalValues.set(organization, nonOverriddenFeatures);
  64. return nonOverriddenFeatures;
  65. }
  66. /**
  67. * Return the effective featureFlags as a map, for the toolbar
  68. */
  69. public getFeatureFlagMap(organization: Organization): FeatureFlagMap {
  70. const nonOverriddenFeatures = this._getNonOverriddenFeatures(organization);
  71. const overrides = this._getStoredOverrides();
  72. const clone: FeatureFlagMap = {...nonOverriddenFeatures};
  73. for (const [name, override] of Object.entries(overrides)) {
  74. clone[name] = {value: clone[name]?.value, override};
  75. }
  76. return clone;
  77. }
  78. /**
  79. * Return the effective featureFlags as an array, for `organization.features`
  80. */
  81. public getEnabledFeatureFlagList(organization: Organization): string[] {
  82. const nonOverriddenFeatures = this._getNonOverriddenFeatures(organization);
  83. const overrides = this._getStoredOverrides();
  84. const names = new Set(Object.keys(nonOverriddenFeatures));
  85. for (const [name, override] of Object.entries(overrides)) {
  86. if (override) {
  87. names.add(name);
  88. } else {
  89. names.delete(name);
  90. }
  91. }
  92. return Array.from(names);
  93. }
  94. /**
  95. * Stash the original list of features & override organization.features with the effective list of features
  96. */
  97. public loadOrg(organization: Organization) {
  98. organization.features = this.getEnabledFeatureFlagList(organization);
  99. }
  100. }