featureObserver.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import type {FeatureFlagContext} from '@sentry/core/build/types/types-hoist/context';
  2. import type {Organization} from 'sentry/types/organization';
  3. import type {Project} from 'sentry/types/project';
  4. const FEATURE_FLAG_BUFFER_SIZE = 100;
  5. let __SINGLETON: FeatureObserver | null = null;
  6. export default class FeatureObserver {
  7. /**
  8. * Return the same instance of FeatureObserver in each part of the app.
  9. * Multiple instances of FeatureObserver are needed by tests only.
  10. */
  11. public static singleton({
  12. bufferSize = FEATURE_FLAG_BUFFER_SIZE,
  13. }: {
  14. bufferSize?: number;
  15. }) {
  16. if (!__SINGLETON) {
  17. __SINGLETON = new FeatureObserver({bufferSize});
  18. }
  19. return __SINGLETON;
  20. }
  21. private _bufferSize = 0;
  22. private FEATURE_FLAGS: FeatureFlagContext = {values: []};
  23. constructor({bufferSize}: {bufferSize: number}) {
  24. this._bufferSize = bufferSize;
  25. }
  26. /**
  27. * Return list of recently accessed feature flags.
  28. */
  29. public getFeatureFlags() {
  30. return this.FEATURE_FLAGS;
  31. }
  32. public updateFlagBuffer({
  33. flagName,
  34. flagResult,
  35. }: {
  36. flagName: string;
  37. flagResult: boolean;
  38. }) {
  39. const flagBuffer = this.FEATURE_FLAGS;
  40. // Check if the flag is already in the buffer
  41. const index = flagBuffer.values.findIndex(f => f.flag === flagName);
  42. // The flag is already in the buffer
  43. if (index !== -1) {
  44. flagBuffer.values.splice(index, 1);
  45. }
  46. // If at capacity, we need to remove the earliest flag
  47. // This will only happen if not a duplicate flag
  48. if (flagBuffer.values.length === this._bufferSize) {
  49. flagBuffer.values.shift();
  50. }
  51. // Store the flag and its result in the buffer
  52. flagBuffer.values.push({
  53. flag: flagName,
  54. result: flagResult,
  55. });
  56. }
  57. public observeOrganizationFlags({organization}: {organization: Organization}) {
  58. // Track names of features that are passed into the .includes() function.
  59. const handler = {
  60. apply: (target: any, orgFeatures: string[], flagName: string[]) => {
  61. // Evaluate the result of .includes()
  62. const flagResult = target.apply(orgFeatures, flagName);
  63. // Append `feature.organizations:` in front to match the Sentry options automator format
  64. const name = 'feature.organizations:' + flagName[0];
  65. this.updateFlagBuffer({
  66. flagName: name,
  67. flagResult,
  68. });
  69. return flagResult;
  70. },
  71. };
  72. const proxy = new Proxy(organization.features.includes, handler);
  73. organization.features.includes = proxy;
  74. }
  75. public observeProjectFlags({project}: {project: Project}) {
  76. // Track names of features that are passed into the .includes() function.
  77. const handler = {
  78. apply: (target: any, projFeatures: string[], flagName: string[]) => {
  79. // Evaluate the result of .includes()
  80. const flagResult = target.apply(projFeatures, flagName);
  81. // Append `feature.projects:` in front to match the Sentry options automator format
  82. const name = 'feature.projects:' + flagName[0];
  83. this.updateFlagBuffer({
  84. flagName: name,
  85. flagResult,
  86. });
  87. return flagResult;
  88. },
  89. };
  90. const proxy = new Proxy(project.features.includes, handler);
  91. project.features.includes = proxy;
  92. }
  93. }