Repository: open-feature/js-sdk
Pull request #660 (2023-11-08 18:28:11)

fix: missing events bundled dep
Fixes: https://github.com/open-feature/js-sdk/issues/659

This PR fixes an issue created with [this change](https://github.com/open-feature/js-sdk/pull/650), which removed the `events` polyfill package from `@openfeature/core` where is wasn't always needed (see that issue for details). The problem was that we still imported `events` in the `@openfeature/core` module, but can't use the `events` bundled in the `@openfeature/web-sdk` since the bundled package there isn't accessible from imports in `@openfeature/core`.

This PR _**removes all imports of `events`**_ from `@openfeature/core`, and instead only imports types. Imports of `events` only now occur in the web-sdk (where it's bundled) and server-sdk (where it's made available by the node runtime), not in the common module.

Unfortunately this issue was a bit tough to track down, because `events` is VERY common, and lots of bundlers, etc will add it, so it's frequently available "accidentally".

Thanks to @juanparadox for the report.

juanparadox: @toddbaert This is awesome. Thank you so much for moving on this so quickly. We were doing some discovery work on available feature flagging tools for the web and this is at the top of our adoption list 😄 
toddbaert: > @toddbaert This is awesome. Thank you so much for moving on this so quickly. We were doing some discovery work on available feature flagging tools for the web and this is at the top of our adoption list 😄

Thanks for finding the issue.

FWIW, I think if you use the last published versions of `@openfeature/core` and `@openfeature/web-sdk`, you will not have this issue. I will try to get this fix released tonight though, once it's reviewed.

Patch:
diff --git a/packages/client/src/client/open-feature-client.ts b/packages/client/src/client/open-feature-client.ts
index 370ec779..e11fc400 100644
--- a/packages/client/src/client/open-feature-client.ts
+++ b/packages/client/src/client/open-feature-client.ts
@@ -11,7 +11,6 @@ import {
   JsonValue,
   Logger,
   OpenFeatureError,
-  OpenFeatureEventEmitter,
   ProviderEvents,
   ProviderStatus,
   ResolutionDetails,
@@ -21,6 +20,7 @@ import {
 import { FlagEvaluationOptions } from '../evaluation';
 import { OpenFeature } from '../open-feature';
 import { Provider } from '../provider';
+import { InternalEventEmitter } from '../events/internal/internal-event-emitter';
 import { Client } from './client';
 
 type OpenFeatureClientOptions = {
@@ -36,7 +36,7 @@ export class OpenFeatureClient implements Client {
     // functions are passed here to make sure that these values are always up to date,
     // and so we don't have to make these public properties on the API class.
     private readonly providerAccessor: () => Provider,
-    private readonly emitterAccessor: () => OpenFeatureEventEmitter,
+    private readonly emitterAccessor: () => InternalEventEmitter,
     private readonly globalLogger: () => Logger,
     private readonly options: OpenFeatureClientOptions
   ) {}
diff --git a/packages/client/src/events/index.ts b/packages/client/src/events/index.ts
new file mode 100644
index 00000000..4377e48f
--- /dev/null
+++ b/packages/client/src/events/index.ts
@@ -0,0 +1,1 @@
+export * from './open-feature-event-emitter';
\ No newline at end of file
diff --git a/packages/client/src/events/internal/internal-event-emitter.ts b/packages/client/src/events/internal/internal-event-emitter.ts
new file mode 100644
index 00000000..e2ce1c5a
--- /dev/null
+++ b/packages/client/src/events/internal/internal-event-emitter.ts
@@ -0,0 +1,8 @@
+import { CommonEventDetails, GenericEventEmitter } from '@openfeature/core';
+
+/**
+ * The InternalEventEmitter is not exported publicly and should only be used within the SDK. It extends the
+ * OpenFeatureEventEmitter to include additional properties that can be included
+ * in the event details.
+ */
+export abstract class InternalEventEmitter extends GenericEventEmitter<CommonEventDetails> {};
\ No newline at end of file
diff --git a/packages/client/src/events/open-feature-event-emitter.ts b/packages/client/src/events/open-feature-event-emitter.ts
new file mode 100644
index 00000000..f9b1b08b
--- /dev/null
+++ b/packages/client/src/events/open-feature-event-emitter.ts
@@ -0,0 +1,20 @@
+import { GenericEventEmitter } from '@openfeature/core';
+import EventEmitter from 'events';
+
+/**
+ * The OpenFeatureEventEmitter can be used by provider developers to emit
+ * events at various parts of the provider lifecycle.
+ *
+ * NOTE: Ready and error events are automatically emitted by the SDK based on
+ * the result of the initialize method.
+ */
+export class OpenFeatureEventEmitter extends GenericEventEmitter {
+  protected readonly eventEmitter = new EventEmitter({ captureRejections: true });
+
+  constructor() {
+    super();
+    this.eventEmitter.on('error', (err) => {
+      this._logger?.error('Error running event handler:', err);
+    });
+  }
+}
diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts
index af13fff4..c6dfb63d 100644
--- a/packages/client/src/index.ts
+++ b/packages/client/src/index.ts
@@ -1,6 +1,6 @@
-import 'events'; // we need this to trigger the bundling of the browser events module: https://www.npmjs.com/package/events
 export * from './client';
 export * from './provider';
 export * from './evaluation';
 export * from './open-feature';
+export * from './events';
 export * from '@openfeature/core';
diff --git a/packages/client/src/open-feature.ts b/packages/client/src/open-feature.ts
index 6b5aa8a9..cb27bd93 100644
--- a/packages/client/src/open-feature.ts
+++ b/packages/client/src/open-feature.ts
@@ -1,6 +1,7 @@
 import { EvaluationContext, ManageContext, OpenFeatureCommonAPI } from '@openfeature/core';
 import { Client, OpenFeatureClient } from './client';
 import { NOOP_PROVIDER, Provider } from './provider';
+import { OpenFeatureEventEmitter } from './events';
 
 // use a symbol as a key for the global singleton
 const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/web-sdk/api');
@@ -11,7 +12,9 @@ type OpenFeatureGlobal = {
 const _globalThis = globalThis as OpenFeatureGlobal;
 
 export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider> implements ManageContext<Promise<void>> {
+  protected _events = new OpenFeatureEventEmitter();
   protected _defaultProvider: Provider = NOOP_PROVIDER;
+  protected _createEventEmitter = () => new OpenFeatureEventEmitter();
 
   // eslint-disable-next-line @typescript-eslint/no-empty-function
   private constructor() {
diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json
index e2c17f0a..0d1267ce 100644
--- a/packages/client/tsconfig.json
+++ b/packages/client/tsconfig.json
@@ -28,7 +28,9 @@
     // "rootDir": "./",                                  /* Specify the root folder within your source files. */
     "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
     // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
-    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
+    "paths": {
+      "@openfeature/core": [ "../shared/src" ]
+    },                                                   /* Specify a set of entries that re-map imports to additional lookup locations. */
     // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
     // "typeRoots": [],                                  /* Specify multiple folders that act like `./node_modules/@types`. */
     // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
diff --git a/packages/server/src/client/open-feature-client.ts b/packages/server/src/client/open-feature-client.ts
index af91e565..9020bbdb 100644
--- a/packages/server/src/client/open-feature-client.ts
+++ b/packages/server/src/client/open-feature-client.ts
@@ -8,7 +8,6 @@ import {
   FlagValueType,
   Hook,
   HookContext,
-  InternalEventEmitter,
   JsonValue,
   Logger,
   ManageContext,
@@ -23,6 +22,7 @@ import { FlagEvaluationOptions } from '../evaluation';
 import { OpenFeature } from '../open-feature';
 import { Provider } from '../provider';
 import { Client } from './client';
+import { InternalEventEmitter } from '../events/internal/internal-event-emitter';
 
 type OpenFeatureClientOptions = {
   name?: string;
diff --git a/packages/server/src/events/index.ts b/packages/server/src/events/index.ts
new file mode 100644
index 00000000..4377e48f
--- /dev/null
+++ b/packages/server/src/events/index.ts
@@ -0,0 +1,1 @@
+export * from './open-feature-event-emitter';
\ No newline at end of file
diff --git a/packages/server/src/events/internal/internal-event-emitter.ts b/packages/server/src/events/internal/internal-event-emitter.ts
new file mode 100644
index 00000000..e2ce1c5a
--- /dev/null
+++ b/packages/server/src/events/internal/internal-event-emitter.ts
@@ -0,0 +1,8 @@
+import { CommonEventDetails, GenericEventEmitter } from '@openfeature/core';
+
+/**
+ * The InternalEventEmitter is not exported publicly and should only be used within the SDK. It extends the
+ * OpenFeatureEventEmitter to include additional properties that can be included
+ * in the event details.
+ */
+export abstract class InternalEventEmitter extends GenericEventEmitter<CommonEventDetails> {};
\ No newline at end of file
diff --git a/packages/server/src/events/open-feature-event-emitter.ts b/packages/server/src/events/open-feature-event-emitter.ts
new file mode 100644
index 00000000..6237bb4f
--- /dev/null
+++ b/packages/server/src/events/open-feature-event-emitter.ts
@@ -0,0 +1,20 @@
+import { GenericEventEmitter } from '@openfeature/core';
+import EventEmitter from 'events';
+
+/**
+ * The OpenFeatureEventEmitter can be used by provider developers to emit
+ * events at various parts of the provider lifecycle.
+ * 
+ * NOTE: Ready and error events are automatically emitted by the SDK based on
+ * the result of the initialize method.
+ */
+export class OpenFeatureEventEmitter extends GenericEventEmitter {
+  protected readonly eventEmitter = new EventEmitter({ captureRejections: true });
+
+   constructor() {
+      super();
+      this.eventEmitter.on('error', (err) => {
+        this._logger?.error('Error running event handler:', err);
+      });
+    }
+};
\ No newline at end of file
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index 3a865a48..d76d3dbd 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -3,4 +3,5 @@ export * from './provider';
 export * from './evaluation';
 export * from './open-feature';
 export * from './transaction-context';
+export * from './events';
 export * from '@openfeature/core';
diff --git a/packages/server/src/open-feature.ts b/packages/server/src/open-feature.ts
index 804c050e..cdaa1793 100644
--- a/packages/server/src/open-feature.ts
+++ b/packages/server/src/open-feature.ts
@@ -13,6 +13,7 @@ import {
   TransactionContextPropagator,
 } from './transaction-context';
 import { Client, OpenFeatureClient } from './client';
+import { OpenFeatureEventEmitter } from './events';
 
 // use a symbol as a key for the global singleton
 const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/js-sdk/api');
@@ -26,8 +27,11 @@ export class OpenFeatureAPI
   extends OpenFeatureCommonAPI<Provider>
   implements ManageContext<OpenFeatureAPI>, ManageTransactionContextPropagator<OpenFeatureCommonAPI<Provider>>
 {
-  private _transactionContextPropagator: TransactionContextPropagator = NOOP_TRANSACTION_CONTEXT_PROPAGATOR;
+  protected _events = new OpenFeatureEventEmitter();
   protected _defaultProvider: Provider = NOOP_PROVIDER;
+  protected _createEventEmitter = () => new OpenFeatureEventEmitter();
+  
+  private _transactionContextPropagator: TransactionContextPropagator = NOOP_TRANSACTION_CONTEXT_PROPAGATOR;
 
   private constructor() {
     super('server');
diff --git a/packages/server/src/provider/in-memory-provider/in-memory-provider.ts b/packages/server/src/provider/in-memory-provider/in-memory-provider.ts
index 00e86850..a52b06c2 100644
--- a/packages/server/src/provider/in-memory-provider/in-memory-provider.ts
+++ b/packages/server/src/provider/in-memory-provider/in-memory-provider.ts
@@ -6,7 +6,6 @@ import {
   JsonValue,
   Logger,
   OpenFeatureError,
-  OpenFeatureEventEmitter,
   ProviderEvents,
   ResolutionDetails,
   StandardResolutionReasons,
@@ -15,6 +14,7 @@ import {
 import { Provider } from '../provider';
 import { Flag, FlagConfiguration } from './flag-configuration';
 import { VariantFoundError } from './variant-not-found-error';
+import { OpenFeatureEventEmitter } from '../..';
 
 /**
  * A simple OpenFeature provider intended for demos and as a test stub.
diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json
index 0e8c768c..1168d18e 100644
--- a/packages/server/tsconfig.json
+++ b/packages/server/tsconfig.json
@@ -27,7 +27,9 @@
     // "rootDir": "./",                                  /* Specify the root folder within your source files. */
     "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
     // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
-    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
+    "paths": {
+      "@openfeature/core": [ "../shared/src" ]
+    },                                                   /* Specify a set of entries that re-map imports to additional lookup locations. */
     // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
     // "typeRoots": [],                                  /* Specify multiple folders that act like `./node_modules/@types`. */
     // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
diff --git a/packages/shared/src/events/open-feature-event-emitter.ts b/packages/shared/src/events/generic-event-emitter.ts
similarity index 64%
rename from packages/shared/src/events/open-feature-event-emitter.ts
rename to packages/shared/src/events/generic-event-emitter.ts
index 56cf4d84..dbeaf889 100644
--- a/packages/shared/src/events/open-feature-event-emitter.ts
+++ b/packages/shared/src/events/generic-event-emitter.ts
@@ -1,21 +1,21 @@
-import EventEmitter from 'events';
 import { Logger, ManageLogger, SafeLogger } from '../logger';
-import { CommonEventDetails, EventContext, EventDetails, EventHandler } from './eventing';
+import { EventContext, EventDetails, EventHandler } from './eventing';
 import { ProviderEvents } from './events';
 
-abstract class GenericEventEmitter<AdditionalContext extends Record<string, unknown> = Record<string, unknown>>
+/**
+ * The GenericEventEmitter should only be used within the SDK. It supports additional properties that can be included
+ * in the event details.
+ */
+export abstract class GenericEventEmitter<AdditionalContext extends Record<string, unknown> = Record<string, unknown>>
   implements ManageLogger<GenericEventEmitter<AdditionalContext>>
 {
+  protected abstract readonly eventEmitter: NodeJS.EventEmitter;
+
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   private readonly _handlers = new WeakMap<EventHandler<any>, EventHandler<any>>();
-  private readonly eventEmitter = new EventEmitter({ captureRejections: true });
   private _eventLogger?: Logger;
 
-  constructor(private readonly globalLogger?: () => Logger) {
-    this.eventEmitter.on('error', (err) => {
-      this._logger?.error('Error running event handler:', err);
-    });
-  }
+  constructor(private readonly globalLogger?: () => Logger) {}
 
   emit<T extends ProviderEvents>(eventType: T, context?: EventContext<T, AdditionalContext>): void {
     this.eventEmitter.emit(eventType, context);
@@ -25,7 +25,7 @@ abstract class GenericEventEmitter<AdditionalContext extends Record<string, unkn
     // The handlers have to be wrapped with an async function because if a synchronous functions throws an error,
     // the other handlers will not run.
     const asyncHandler = async (details?: EventDetails<T>) => {
-      await handler(details);
+      await handler(details);    
     };
     // The async handler has to be written to the map, because we need to get the wrapper function when deleting a listener
     this._handlers.set(handler, asyncHandler);
@@ -61,23 +61,7 @@ abstract class GenericEventEmitter<AdditionalContext extends Record<string, unkn
     return this;
   }
 
-  private get _logger() {
+  protected get _logger() {
     return this._eventLogger ?? this.globalLogger?.();
   }
-}
-
-/**
- * The OpenFeatureEventEmitter can be used by provider developers to emit
- * events at various parts of the provider lifecycle.
- * 
- * NOTE: Ready and error events are automatically emitted by the SDK based on
- * the result of the initialize method.
- */
-export class OpenFeatureEventEmitter extends GenericEventEmitter {};
-
-/**
- * The InternalEventEmitter should only be used within the SDK. It extends the
- * OpenFeatureEventEmitter to include additional properties that can be included
- * in the event details.
- */
-export class InternalEventEmitter extends GenericEventEmitter<CommonEventDetails> {};
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/packages/shared/src/events/index.ts b/packages/shared/src/events/index.ts
index c32213c3..3759ee4b 100644
--- a/packages/shared/src/events/index.ts
+++ b/packages/shared/src/events/index.ts
@@ -1,4 +1,4 @@
 export * from './event-utils';
 export * from './eventing';
 export * from './events';
-export * from './open-feature-event-emitter';
+export * from './generic-event-emitter';
diff --git a/packages/shared/src/open-feature.ts b/packages/shared/src/open-feature.ts
index 1f6f23ee..dbf27452 100644
--- a/packages/shared/src/open-feature.ts
+++ b/packages/shared/src/open-feature.ts
@@ -1,7 +1,7 @@
 import { GeneralError } from './errors';
 import { EvaluationContext, FlagValue } from './evaluation';
 import { EventDetails, EventHandler, Eventing, ProviderEvents, statusMatchesEvent } from './events';
-import { InternalEventEmitter } from './events/open-feature-event-emitter';
+import { GenericEventEmitter } from './events';
 import { isDefined } from './filter';
 import { EvaluationLifeCycle, Hook } from './hooks';
 import { DefaultLogger, Logger, ManageLogger, SafeLogger } from './logger';
@@ -12,17 +12,18 @@ import { Paradigm } from './types';
 export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProvider>
   implements Eventing, EvaluationLifeCycle<OpenFeatureCommonAPI<P>>, ManageLogger<OpenFeatureCommonAPI<P>>
 {
+  protected abstract _createEventEmitter(): GenericEventEmitter;
+  protected abstract _defaultProvider: P;
+  protected abstract readonly _events: GenericEventEmitter;
+
   protected _hooks: Hook[] = [];
   protected _context: EvaluationContext = {};
   protected _logger: Logger = new DefaultLogger();
 
-  protected abstract _defaultProvider: P;
-
-  private readonly _events = new InternalEventEmitter(() => this._logger);
   private readonly _clientEventHandlers: Map<string | undefined, [ProviderEvents, EventHandler<ProviderEvents>][]> =
     new Map();
   protected _clientProviders: Map<string, P> = new Map();
-  protected _clientEvents: Map<string | undefined, InternalEventEmitter> = new Map();
+  protected _clientEvents: Map<string | undefined, GenericEventEmitter> = new Map();
   protected _runsOn: Paradigm;
 
   constructor(category: Paradigm) {
@@ -238,7 +239,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
     return this._clientProviders.get(name) ?? this._defaultProvider;
   }
 
-  protected buildAndCacheEventEmitterForClient(name?: string): InternalEventEmitter {
+  protected buildAndCacheEventEmitterForClient(name?: string): GenericEventEmitter {
     const emitter = this._clientEvents.get(name);
 
     if (emitter) {
@@ -246,7 +247,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
     }
 
     // lazily add the event emitters
-    const newEmitter = new InternalEventEmitter(() => this._logger);
+    const newEmitter = this._createEventEmitter();
     this._clientEvents.set(name, newEmitter);
 
     const clientProvider = this.getProviderForClient(name);
@@ -259,7 +260,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
     return newEmitter;
   }
 
-  private getUnboundEmitters(): InternalEventEmitter[] {
+  private getUnboundEmitters(): GenericEventEmitter[] {
     const namedProviders = [...this._clientProviders.keys()];
     const eventEmitterNames = [...this._clientEvents.keys()].filter(isDefined);
     const unboundEmitterNames = eventEmitterNames.filter((name) => !namedProviders.includes(name));
@@ -279,7 +280,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
     oldProvider: P,
     newProvider: P,
     clientName: string | undefined,
-    emitters: (InternalEventEmitter | undefined)[]
+    emitters: (GenericEventEmitter | undefined)[]
   ) {
     this._clientEventHandlers
       .get(clientName)
diff --git a/packages/shared/src/provider/provider.ts b/packages/shared/src/provider/provider.ts
index e67b78c3..84ef8d48 100644
--- a/packages/shared/src/provider/provider.ts
+++ b/packages/shared/src/provider/provider.ts
@@ -1,5 +1,5 @@
 import { EvaluationContext } from '../evaluation';
-import { OpenFeatureEventEmitter } from '../events';
+import { GenericEventEmitter } from '../events';
 import { Metadata, Paradigm } from '../types';
 
 /**
@@ -57,7 +57,7 @@ export interface CommonProvider {
    * An event emitter for ProviderEvents.
    * @see ProviderEvents
    */
-  events?: OpenFeatureEventEmitter;
+  events?: GenericEventEmitter;
 
   /**
    * A function used to shut down the provider.


Test patch:
diff --git a/packages/client/test/events.spec.ts b/packages/client/test/events.spec.ts
index e445cae0..a7d93e2e 100644
--- a/packages/client/test/events.spec.ts
+++ b/packages/client/test/events.spec.ts
@@ -9,7 +9,7 @@ import {
   ProviderMetadata,
   ProviderStatus,
   ResolutionDetails,
-  StaleEvent,
+  StaleEvent
 } from '../src';
 
 const TIMEOUT = 1000;
diff --git a/packages/server/test/events.spec.ts b/packages/server/test/events.spec.ts
index 55f076d0..42cbfbfb 100644
--- a/packages/server/test/events.spec.ts
+++ b/packages/server/test/events.spec.ts
@@ -9,7 +9,7 @@ import {
   ProviderMetadata,
   ProviderStatus,
   ResolutionDetails,
-  StaleEvent,
+  StaleEvent
 } from '../src';
 
 const TIMEOUT = 1000;
@@ -184,7 +184,7 @@ describe('Events', () => {
           OpenFeature.addHandler(ProviderEvents.Error, () => {
             resolve();
           });
-        })
+        }),
       ]).then(() => {
         done();
       });
@@ -312,7 +312,11 @@ describe('Events', () => {
     });
 
     it('handler added while while provider initializing runs', (done) => {
-      const provider = new MockProvider({ name: 'race', initialStatus: ProviderStatus.NOT_READY, initDelay: TIMEOUT / 2 });
+      const provider = new MockProvider({
+        name: 'race',
+        initialStatus: ProviderStatus.NOT_READY,
+        initDelay: TIMEOUT / 2,
+      });
 
       // set the default provider
       OpenFeature.setProvider(provider);
@@ -505,12 +509,12 @@ describe('Events', () => {
     describe('API', () => {
       it('Handlers attached after the provider is already in the associated state, MUST run immediately.', (done) => {
         const provider = new MockProvider({ initialStatus: ProviderStatus.ERROR });
-  
+
         OpenFeature.setProvider(clientId, provider);
         expect(provider.initialize).not.toHaveBeenCalled();
-  
+
         OpenFeature.addHandler(ProviderEvents.Error, () => {
-            done();
+          done();
         });
       });
     });
@@ -519,14 +523,14 @@ describe('Events', () => {
       it('Handlers attached after the provider is already in the associated state, MUST run immediately.', (done) => {
         const provider = new MockProvider({ initialStatus: ProviderStatus.READY });
         const client = OpenFeature.getClient(clientId);
-  
+
         OpenFeature.setProvider(clientId, provider);
         expect(provider.initialize).not.toHaveBeenCalled();
-  
+
         client.addHandler(ProviderEvents.Ready, () => {
-            done();
+          done();
         });
       });
-    });    
+    });
   });
 });
diff --git a/packages/server/test/in-memory-provider.spec.ts b/packages/server/test/in-memory-provider.spec.ts
index 48be6448..46c9d96c 100644
--- a/packages/server/test/in-memory-provider.spec.ts
+++ b/packages/server/test/in-memory-provider.spec.ts
@@ -1,4 +1,4 @@
-import { ErrorCode, FlagNotFoundError, ProviderEvents, StandardResolutionReasons, TypeMismatchError } from '@openfeature/core';
+import { FlagNotFoundError, ProviderEvents, StandardResolutionReasons, TypeMismatchError } from '@openfeature/core';
 import { InMemoryProvider } from '../src';
 import { VariantFoundError } from '../src/provider/in-memory-provider/variant-not-found-error';
 
diff --git a/packages/shared/test/events.spec.ts b/packages/shared/test/events.spec.ts
index 40eee52b..9742f786 100644
--- a/packages/shared/test/events.spec.ts
+++ b/packages/shared/test/events.spec.ts
@@ -1,9 +1,22 @@
-import { OpenFeatureEventEmitter, ProviderEvents, Logger, ReadyEvent } from '../src';
+import EventEmitter from 'events';
+import { GenericEventEmitter, Logger, ProviderEvents, ReadyEvent } from '../src';
 
-describe('OpenFeatureEventEmitter', () => {
+// create concrete class to test the abstract functionality.
+class TestEventEmitter extends GenericEventEmitter {
+  protected readonly eventEmitter = new EventEmitter({ captureRejections: true });
+
+  constructor() {
+    super();
+    this.eventEmitter.on('error', (err) => {
+      this._logger?.error('Error running event handler:', err);
+    });
+  }
+}
+
+describe('GenericEventEmitter', () => {
   describe('addHandler should', function () {
     it('attach handler for event type', function () {
-      const emitter = new OpenFeatureEventEmitter();
+      const emitter = new TestEventEmitter();
 
       const handler1 = jest.fn();
       emitter.addHandler(ProviderEvents.Ready, handler1);
@@ -13,7 +26,7 @@ describe('OpenFeatureEventEmitter', () => {
     });
 
     it('attach several handlers for event type', function () {
-      const emitter = new OpenFeatureEventEmitter();
+      const emitter = new TestEventEmitter();
 
       const handler1 = jest.fn();
       const handler2 = jest.fn();
@@ -40,7 +53,7 @@ describe('OpenFeatureEventEmitter', () => {
         debug: () => done(),
       };
 
-      const emitter = new OpenFeatureEventEmitter();
+      const emitter = new TestEventEmitter();
       emitter.setLogger(logger);
 
       emitter.addHandler(ProviderEvents.Ready, async () => {
@@ -50,7 +63,7 @@ describe('OpenFeatureEventEmitter', () => {
     });
 
     it('trigger handler for event type', function () {
-      const emitter = new OpenFeatureEventEmitter();
+      const emitter = new TestEventEmitter();
 
       const handler1 = jest.fn();
       emitter.addHandler(ProviderEvents.Ready, handler1);
@@ -61,7 +74,7 @@ describe('OpenFeatureEventEmitter', () => {
 
     it('trigger handler for event type with event data', function () {
       const event: ReadyEvent = { message: 'message' };
-      const emitter = new OpenFeatureEventEmitter();
+      const emitter = new TestEventEmitter();
 
       const handler1 = jest.fn();
       emitter.addHandler(ProviderEvents.Ready, handler1);
@@ -71,7 +84,7 @@ describe('OpenFeatureEventEmitter', () => {
     });
 
     it('trigger several handlers for event type', function () {
-      const emitter = new OpenFeatureEventEmitter();
+      const emitter = new TestEventEmitter();
 
       const handler1 = jest.fn();
       const handler2 = jest.fn();
@@ -91,7 +104,7 @@ describe('OpenFeatureEventEmitter', () => {
 
   describe('removeHandler should', () => {
     it('remove single handler', function () {
-      const emitter = new OpenFeatureEventEmitter();
+      const emitter = new TestEventEmitter();
 
       const handler1 = jest.fn();
       emitter.addHandler(ProviderEvents.Ready, handler1);
@@ -106,7 +119,7 @@ describe('OpenFeatureEventEmitter', () => {
 
   describe('removeAllHandlers should', () => {
     it('remove all handlers for event type', function () {
-      const emitter = new OpenFeatureEventEmitter();
+      const emitter = new TestEventEmitter();
 
       const handler1 = jest.fn();
       const handler2 = jest.fn();
@@ -122,7 +135,7 @@ describe('OpenFeatureEventEmitter', () => {
     });
 
     it('remove all handlers only for event type', function () {
-      const emitter = new OpenFeatureEventEmitter();
+      const emitter = new TestEventEmitter();
 
       const handler1 = jest.fn();
       const handler2 = jest.fn();
@@ -138,7 +151,7 @@ describe('OpenFeatureEventEmitter', () => {
     });
 
     it('remove all handlers if no event type is given', function () {
-      const emitter = new OpenFeatureEventEmitter();
+      const emitter = new TestEventEmitter();
 
       const handler1 = jest.fn();
       const handler2 = jest.fn();