Repository: sindresorhus/emittery (js)

Issue:
Built-in debug info
<!-- Issuehunt Badges -->
[<img alt="Issuehunt badges" src="https://img.shields.io/badge/IssueHunt-%2440%20Funded-%2300A156.svg" />](https://issuehunt.io/r/sindresorhus/emittery/issues/2)
<!-- /Issuehunt Badges -->


Running `DEBUG=emittery node x.js` or `DEBUG=* node x.js` should output debug information/trace of events.

Events can be hard to debug, so this is important.

I'm looking for feedback on what the trace output should look like.

---

*I don't want to depend on the `debug` module, but we could easily detect the environment variable manually.*


<!-- Issuehunt content -->

---

<details>
<summary>
<b>IssueHunt Summary</b>
</summary>


### Backers (Total: $40.00)

- [<img src='https://avatars3.githubusercontent.com/u/44827199?v=4' alt='issuehunt' width=24 height=24> issuehunt](https://issuehunt.io/u/issuehunt) ($40.00)

### Submitted pull Requests
- [#53 Implements debug feature as described in #2](https://issuehunt.io/r/sindresorhus/emittery/pull/53)
---

#### [Become a backer now!](https://issuehunt.io/r/sindresorhus/emittery/issues/2)
#### [Or submit a pull request to get the deposits!](https://issuehunt.io/r/sindresorhus/emittery/issues/2)
### Tips

- Checkout the [Issuehunt explorer](https://issuehunt.io/r/sindresorhus/emittery/) to discover more funded issues.
- Need some help from other developers? [Add your repositories](https://issuehunt.io/r/new) on IssueHunt to raise funds.
---
IssueHunt has been backed by the following sponsors. [Become a sponsor](https://issuehunt.io/membership/members)
</details>
<!-- /Issuehunt content-->

Pull request:
Add debug functionality
This PR aims to implement a built-in debug feature described in #2
#### How it's implemented
There's a static function in the `Emittery` class called `debugLogger`, that takes in 4 parameters
* type - type of debug log
* debugName - set by the instance of Emittery that generated this log
* eventName - event name related to the log
* eventData - in case of emit, this is the data sent by it

Debugging can be triggered in 3 ways currently:
1. the `DEBUG` environment variable is set to *emittery* or *
2. the static `isDebug` function in Emittery returns true (by default this handles the environment variable, but this function can be overridden, to enable package wide debugging instead of the 3rd method.
3. the `isDebug` variable is set to true on one or more instances.
#### Questions/suggestions
I wasn't sure if the default logger function should include some sort of timestamp.

It might add too much complexity but a verbosity option could be nice. For example currently every emit, subscribe, unsubscribe etc. event is logged, but in some cases they might not all be interesting. Of course this can be achieved by overriding the `debugLogger` function on `Emittery`, but it would be a nice convenience option.

Is it acceptable that there's one static logging function on `Emittery` or should the individual instanced of it be able to override this function. This would add the ability for different instances of `Emittery` to log differently based on their function in an application.

Fixes #2


<!-- Issuehunt content -->

---

<details>
<summary>
<b>IssueHunt Summary</b>
</summary>

### Referenced issues

This pull request has been submitted to:
- [#2: Built-in debug info](https://issuehunt.io/repos/112248999/issues/2)
---

IssueHunt has been backed by the following sponsors. [Become a sponsor](https://issuehunt.io/membership/members)
</details>
<!-- /Issuehunt content-->

Patch:
diff --git a/index.d.ts b/index.d.ts
index 37da6a1..66036e9 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -14,6 +14,112 @@ declare const listenerAdded: unique symbol;
 declare const listenerRemoved: unique symbol;
 type OmnipresentEventData = {[listenerAdded]: Emittery.ListenerChangedData; [listenerRemoved]: Emittery.ListenerChangedData};
 
+/**
+Emittery can collect and log debug information.
+
+To enable this feature set the DEBUG environment variable to 'emittery' or '*'. Additionally you can set the static `isDebugEnabled` variable to true on the Emittery class, or `myEmitter.debug.enabled` on an instance of it for debugging a single instance.
+
+See API for more information on how debugging works.
+*/
+type DebugLogger<EventData, Name extends keyof EventData> = (type: string, debugName: string, eventName?: Name, eventData?: EventData[Name]) => void;
+
+/**
+Configure debug options of an instance.
+*/
+interface DebugOptions<EventData> {
+	/**
+	Define a name for the instance of Emittery to use when outputting debug data.
+
+	@default undefined
+
+	@example
+	```
+	const Emittery = require('emittery');
+	Emittery.isDebugEnabled = true;
+
+	const emitter = new Emittery({debug: {name: 'myEmitter'}});
+	emitter.on('test', data => { // do something });
+
+	emitter.emit('test');
+
+	//=> [16:43:20.417][emittery:subscribe][myEmitter] Event Name: test
+	//	data: undefined
+	```
+	*/
+	readonly name: string;
+
+	/**
+	Toggle debug logging just for this instance.
+
+	@default false
+
+	@example
+	```
+	const Emittery = require('emittery');
+
+	const emitter1 = new Emittery({debug: {name: 'emitter1', enabled: true}});
+	const emitter2 = new Emittery({debug: {name: 'emitter2'}});
+
+	emitter1.on('test', data => { // do something });
+	emitter2.on('test', data => { // do something });
+
+	emitter1.emit('test');
+	emitter2.emit('test');
+
+	//=> [16:43:20.417][emittery:subscribe][emitter1] Event Name: test
+	//	data: undefined
+	```
+	*/
+	enabled?: boolean;
+
+	/**
+	Function that handles debug data.
+
+	@default
+	```
+	(type, debugName, eventName, eventData) => {
+		eventData = JSON.stringify(eventData);
+
+		if (typeof eventName === 'symbol') {
+			eventName = eventName.toString();
+		}
+
+		const currentTime = new Date();
+		const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;
+		console.log(`[${logTime}][emittery:${type}][${debugName}] Event Name: ${eventName}\n\tdata: ${eventData}`);
+	}
+	```
+
+	@example
+	```
+	const Emittery = require('emittery');
+	const myLogger = (type, debugName, eventName, eventData) => console.log(`[${type}]: ${eventName}`);
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'myEmitter',
+			enabled: true,
+			logger: myLogger
+		}
+	});
+
+	emitter.on('test', data => { // do something });
+
+	emitter.emit('test');
+
+	//=> [subscribe]: test
+	```
+	*/
+	logger?: DebugLogger<EventData, keyof EventData>;
+}
+
+/**
+Configuration options for Emittery.
+*/
+interface Options<EventData> {
+	debug?: DebugOptions<EventData>;
+}
+
 /**
 Emittery is a strictly typed, fully async EventEmitter implementation. Event listeners can be registered with `on` or `once`, and events can be emitted with `emit`.
 
@@ -50,6 +156,34 @@ declare class Emittery<
 	AllEventData = EventData & OmnipresentEventData,
 	DatalessEvents = DatalessEventNames<EventData>
 > {
+	/**
+	Toggle debug mode for all instances.
+
+	@default Returns true if the DEBUG environment variable is set to 'emittery' or '*', otherwise false.
+
+	@example
+	```
+	const Emittery = require('emittery');
+	Emittery.isDebugEnabled = true;
+
+	const emitter1 = new Emittery({debug: {name: 'myEmitter1'}});
+	const emitter2 = new Emittery({debug: {name: 'myEmitter2'}});
+
+	emitter1.on('test', data => { // do something });
+	emitter2.on('otherTest', data => { // do something });
+
+	emitter1.emit('test');
+	emitter2.emit('otherTest');
+
+	//=> [16:43:20.417][emittery:subscribe][myEmitter1] Event Name: test
+	//	data: undefined
+
+	//=> [16:43:20.417][emittery:subscribe][myEmitter2] Event Name: otherTest
+	//	data: undefined
+	```
+	*/
+	static isDebugEnabled: boolean;
+
 	/**
 	Fires when an event listener was added.
 
@@ -104,6 +238,18 @@ declare class Emittery<
 	*/
 	static readonly listenerRemoved: typeof listenerRemoved;
 
+	/**
+	Debugging options for the current instance.
+	*/
+	debug: DebugOptions<EventData>;
+
+	/**
+	Create a new Emittery instance with the specified options.
+
+	@returns An instance of Emittery that you can use to listen for and emit events.
+	*/
+	constructor(options?: Options<EventData>);
+
 	/**
 	In TypeScript, it returns a decorator which mixins `Emittery` as property `emitteryPropertyName` and `methodNames`, or all `Emittery` methods if `methodNames` is not defined, into the target class.
 
diff --git a/index.js b/index.js
index 293b664..f0be5f2 100644
--- a/index.js
+++ b/index.js
@@ -9,6 +9,8 @@ const resolvedPromise = Promise.resolve();
 const listenerAdded = Symbol('listenerAdded');
 const listenerRemoved = Symbol('listenerRemoved');
 
+let isGlobalDebugEnabled = false;
+
 function assertEventName(eventName) {
 	if (typeof eventName !== 'string' && typeof eventName !== 'symbol') {
 		throw new TypeError('eventName must be a string or a symbol');
@@ -189,10 +191,43 @@ class Emittery {
 		};
 	}
 
-	constructor() {
+	static get isDebugEnabled() {
+		return process.env.DEBUG === 'emittery' || process.env.DEBUG === '*' || isGlobalDebugEnabled;
+	}
+
+	static set isDebugEnabled(newValue) {
+		isGlobalDebugEnabled = newValue;
+	}
+
+	constructor(options = {}) {
 		anyMap.set(this, new Set());
 		eventsMap.set(this, new Map());
 		producersMap.set(this, new Map());
+		this.debug = options.debug || {};
+
+		if (this.debug.enabled === undefined) {
+			this.debug.enabled = false;
+		}
+
+		if (!this.debug.logger) {
+			this.debug.logger = (type, debugName, eventName, eventData) => {
+				eventData = JSON.stringify(eventData);
+
+				if (typeof eventName === 'symbol') {
+					eventName = eventName.toString();
+				}
+
+				const currentTime = new Date();
+				const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;
+				console.log(`[${logTime}][emittery:${type}][${debugName}] Event Name: ${eventName}\n\tdata: ${eventData}`);
+			};
+		}
+	}
+
+	logIfDebugEnabled(type, eventName, eventData) {
+		if (Emittery.isDebugEnabled || this.debug.enabled) {
+			this.debug.logger(type, this.debug.name, eventName, eventData);
+		}
 	}
 
 	on(eventNames, listener) {
@@ -203,6 +238,8 @@ class Emittery {
 			assertEventName(eventName);
 			getListeners(this, eventName).add(listener);
 
+			this.logIfDebugEnabled('subscribe', eventName, undefined);
+
 			if (!isListenerSymbol(eventName)) {
 				this.emit(listenerAdded, {eventName, listener});
 			}
@@ -219,6 +256,8 @@ class Emittery {
 			assertEventName(eventName);
 			getListeners(this, eventName).delete(listener);
 
+			this.logIfDebugEnabled('unsubscribe', eventName, undefined);
+
 			if (!isListenerSymbol(eventName)) {
 				this.emit(listenerRemoved, {eventName, listener});
 			}
@@ -246,6 +285,8 @@ class Emittery {
 	async emit(eventName, eventData) {
 		assertEventName(eventName);
 
+		this.logIfDebugEnabled('emit', eventName, eventData);
+
 		enqueueProducers(this, eventName, eventData);
 
 		const listeners = getListeners(this, eventName);
@@ -271,6 +312,8 @@ class Emittery {
 	async emitSerial(eventName, eventData) {
 		assertEventName(eventName);
 
+		this.logIfDebugEnabled('emitSerial', eventName, eventData);
+
 		const listeners = getListeners(this, eventName);
 		const anyListeners = anyMap.get(this);
 		const staticListeners = [...listeners];
@@ -294,6 +337,9 @@ class Emittery {
 
 	onAny(listener) {
 		assertListener(listener);
+
+		this.logIfDebugEnabled('subscribeAny', undefined, undefined);
+
 		anyMap.get(this).add(listener);
 		this.emit(listenerAdded, {listener});
 		return this.offAny.bind(this, listener);
@@ -305,6 +351,9 @@ class Emittery {
 
 	offAny(listener) {
 		assertListener(listener);
+
+		this.logIfDebugEnabled('unsubscribeAny', undefined, undefined);
+
 		this.emit(listenerRemoved, {listener});
 		anyMap.get(this).delete(listener);
 	}
@@ -313,6 +362,8 @@ class Emittery {
 		eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
 
 		for (const eventName of eventNames) {
+			this.logIfDebugEnabled('clear', eventName, undefined);
+
 			if (typeof eventName === 'string' || typeof eventName === 'symbol') {
 				getListeners(this, eventName).clear();
 
diff --git a/readme.md b/readme.md
index 2ae169a..816492b 100644
--- a/readme.md
+++ b/readme.md
@@ -44,7 +44,141 @@ Emittery accepts strings and symbols as event names.
 
 Symbol event names can be used to avoid name collisions when your classes are extended, especially for internal events.
 
-### emitter = new Emittery()
+### isDebugEnabled
+
+Toggle debug mode for all instances.
+
+Default: Returns true if the DEBUG environment variable is set to 'emittery' or '*', otherwise false.
+
+Example:
+
+```js
+const Emittery = require('emittery');
+
+Emittery.isDebugEnabled = true;
+
+const emitter1 = new Emittery({debug: {name: 'myEmitter1'}});
+const emitter2 = new Emittery({debug: {name: 'myEmitter2'}});
+
+emitter1.on('test', data => { // do something });
+emitter2.on('otherTest', data => { // do something });
+
+emitter1.emit('test');
+emitter2.emit('otherTest');
+
+//=> [16:43:20.417][emittery:subscribe][myEmitter1] Event Name: test
+//	data: undefined
+
+//=> [16:43:20.417][emittery:subscribe][myEmitter2] Event Name: otherTest
+//	data: undefined
+```
+
+### emitter = new Emittery(options?)
+
+Create a new instance of Emittery.
+
+#### options?
+
+Configure the new instance of Emittery.
+
+##### debug?
+
+Configure the debugging options for this instance.
+
+###### name
+
+Type: `string`\
+Default: `undefined`
+
+Define a name for the instance of Emittery to use when outputting debug data.
+
+Example:
+
+```js
+const Emittery = require('emittery');
+
+Emittery.isDebugEnabled = true;
+
+const emitter = new Emittery({debug: {name: 'myEmitter'}});
+emitter.on('test', data => { // do something });
+
+emitter.emit('test');
+
+//=> [16:43:20.417][emittery:subscribe][myEmitter] Event Name: test
+//	data: undefined
+```
+
+###### enabled?
+
+Type: `boolean`
+
+Default: `false`
+
+Toggle debug logging just for this instance.
+
+Example:
+
+```js
+const Emittery = require('emittery');
+
+const emitter1 = new Emittery({debug: {name: 'emitter1', enabled: true}});
+const emitter2 = new Emittery({debug: {name: 'emitter2'}});
+
+emitter1.on('test', data => { // do something });
+emitter2.on('test', data => { // do something });
+
+emitter1.emit('test');
+emitter2.emit('test');
+
+//=> [16:43:20.417][emittery:subscribe][emitter1] Event Name: test
+//	data: undefined
+```
+
+###### logger?
+
+Type: `Function(string, string, EventName?, Record<string, any>?) => void`
+
+Default:
+
+```js
+(type, debugName, eventName, eventData) => {
+	if (typeof eventData === 'object') {
+		eventData = JSON.stringify(eventData);
+	}
+
+	if (typeof eventName === 'symbol') {
+		eventName = eventName.toString();
+	}
+
+	const currentTime = new Date();
+	const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;
+	console.log(`[${logTime}][emittery:${type}][${debugName}] Event Name: ${eventName}\n\tdata: ${eventData}`);
+}
+```
+
+Function that handles debug data.
+
+Example:
+
+```js
+const Emittery = require('emittery');
+
+const myLogger = (type, debugName, eventName, eventData) => console.log(`[${type}]: ${eventName}`);
+
+const emitter = new Emittery({
+	debug: {
+		name: 'myEmitter',
+		enabled: true,
+		logger: myLogger
+	}
+});
+
+emitter.on('test', data => { // do something });
+
+emitter.emit('test');
+
+//=> [subscribe]: test
+```
 
 #### on(eventName | eventName[], listener)
 
@@ -365,6 +499,14 @@ Listeners are not invoked for events emitted *before* the listener was added. Re
 
 Note that when using `.emitSerial()`, a slow listener will delay invocation of subsequent listeners. It's possible for newer events to overtake older ones.
 
+## Debugging
+
+Emittery can collect and log debug information.
+
+To enable this feature set the DEBUG environment variable to 'emittery' or '*'. Additionally you can set the static `isDebugEnabled` variable to true on the Emittery class, or `myEmitter.debug.enabled` on an instance of it for debugging a single instance.
+
+See [API](#api) for more details on how debugging works.
+
 ## FAQ
 
 ### How is this different than the built-in `EventEmitter` in Node.js?


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index e3c7e36..75f0243 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -1,5 +1,5 @@
 /* eslint-disable @typescript-eslint/no-empty-function */
-import {expectType, expectError} from 'tsd';
+import {expectType, expectError, expectNotAssignable, expectAssignable} from 'tsd';
 import pEvent = require('p-event');
 import Emittery = require('.');
 
@@ -66,6 +66,41 @@ type AnyListener = (eventData?: unknown) => void | Promise<void>;
 	expectError(ee.on('anEvent', (data: any, more: any) => undefined));
 }
 
+// IsDebug
+{
+	type MyEventData = {
+		value: string;
+		open: undefined;
+		close: boolean;
+	};
+
+	const ee = new Emittery<MyEventData>();
+
+	const myLogger = (type: string, debugName: string, eventName?: keyof MyEventData, eventData?: MyEventData[keyof MyEventData]): void => {
+		expectAssignable<string>(type);
+		expectAssignable<string>(debugName);
+		expectAssignable<string | undefined>(eventName);
+		expectAssignable<MyEventData[keyof MyEventData]>(eventData);
+	};
+
+	const debugOptions = {name: 'test', enabled: true, logger: myLogger};
+
+	// Global debug flag
+	expectAssignable<boolean>(Emittery.isDebugEnabled);
+
+	// General debug options
+	expectAssignable<typeof ee.debug>(debugOptions);
+	expectAssignable<string>(ee.debug.name);
+	expectAssignable<boolean | undefined>(ee.debug.enabled);
+
+	// Debug logger
+	expectNotAssignable<() => undefined>(ee.debug.logger);
+	expectNotAssignable<(data: unknown) => undefined>(ee.debug.logger);
+	expectNotAssignable<(type: string, debugName: string) => undefined>(ee.debug.logger);
+	expectNotAssignable<((type: string, debugName: string, eventName?: string, eventData?: Record<string, any>) => void) | undefined>(ee.debug.logger);
+	expectAssignable<typeof ee.debug.logger>(myLogger);
+}
+
 // Userland can't emit the meta events
 {
 	const ee = new Emittery();
diff --git a/test/index.js b/test/index.js
index 5d8001b..9ec6116 100644
--- a/test/index.js
+++ b/test/index.js
@@ -102,6 +102,27 @@ test('off() - listenerAdded', t => {
 	t.pass();
 });
 
+test('off() - isDebug logs output', t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	const off = emitter.on('test', () => {});
+	off();
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'unsubscribe');
+	t.is(eventStore[2].eventName, 'test');
+	t.is(eventStore[2].debugName, 'testEmitter');
+});
+
 test('on() - listenerAdded offAny', async t => {
 	const emitter = new Emittery();
 	const addListener = () => 1;
@@ -161,6 +182,27 @@ test('on() - dedupes identical listeners', async t => {
 	t.deepEqual(calls, [1]);
 });
 
+test('on() - isDebug logs output', t => {
+	const eventStore = [];
+	const calls = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	emitter.on('test', data => calls.push(data));
+	t.true(eventStore.length > 0);
+	t.is(eventStore[0].type, 'subscribe');
+	t.is(eventStore[0].debugName, 'testEmitter');
+	t.is(eventStore[0].eventName, 'test');
+});
+
 test.serial('events()', async t => {
 	const emitter = new Emittery();
 	const iterator = emitter.events('🦄');
@@ -471,6 +513,28 @@ test('emit() - calls listeners subscribed when emit() was invoked', async t => {
 	t.deepEqual(calls, [1, 1, 2, 3, 2, 4, 5, 2, 4, 7, 6, 2, 4, 7, 6, 9, 2, 4, 7, 6, 9]);
 });
 
+test('emit() - isDebug logs output', async t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	emitter.on('test', () => {});
+	await emitter.emit('test', 'data');
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'emit');
+	t.is(eventStore[2].eventName, 'test');
+	t.is(eventStore[2].debugName, 'testEmitter');
+	t.is(eventStore[2].eventData, 'data');
+});
+
 test('emit() - returns undefined', async t => {
 	const emitter = new Emittery();
 
@@ -611,6 +675,28 @@ test('emitSerial() - calls listeners subscribed when emitSerial() was invoked',
 	t.deepEqual(calls, [1, 1, 2, 3, 2, 4, 5, 2, 4, 7, 6, 2, 4, 7, 6, 9, 2, 4, 7, 6, 9]);
 });
 
+test('emitSerial() - isDebug logs output', async t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	emitter.on('test', () => {});
+	await emitter.emitSerial('test', 'data');
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'emitSerial');
+	t.is(eventStore[2].eventName, 'test');
+	t.is(eventStore[2].debugName, 'testEmitter');
+	t.is(eventStore[2].eventData, 'data');
+});
+
 test('onAny()', async t => {
 	t.plan(4);
 
@@ -825,6 +911,68 @@ test('clearListeners() - with event name - clears iterators for that event', asy
 	t.deepEqual(await anyIterator.next(), {done: false, value: ['🌈', '💫']});
 });
 
+test('clearListeners() - isDebug logs output', t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	emitter.on('test', () => {});
+	emitter.clearListeners('test');
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'clear');
+	t.is(eventStore[2].eventName, 'test');
+	t.is(eventStore[2].debugName, 'testEmitter');
+});
+
+test('onAny() - isDebug logs output', t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	emitter.onAny(() => {});
+	t.true(eventStore.length > 0);
+	t.is(eventStore[0].type, 'subscribeAny');
+	t.is(eventStore[0].eventName, undefined);
+	t.is(eventStore[0].debugName, 'testEmitter');
+});
+
+test('offAny() - isDebug logs output', t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: true,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	const off = emitter.onAny(() => {});
+	off();
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'unsubscribeAny');
+	t.is(eventStore[2].eventName, undefined);
+	t.is(eventStore[2].debugName, 'testEmitter');
+});
+
 test('listenerCount()', t => {
 	const emitter = new Emittery();
 	emitter.on('🦄', () => {});
@@ -911,7 +1059,7 @@ test('bindMethods() - methodNames must be array of strings or undefined', t => {
 });
 
 test('bindMethods() - must bind all methods if no array supplied', t => {
-	const methodsExpected = ['on', 'off', 'once', 'events', 'emit', 'emitSerial', 'onAny', 'anyEvent', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods'];
+	const methodsExpected = ['on', 'off', 'once', 'events', 'emit', 'emitSerial', 'onAny', 'anyEvent', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods', 'logIfDebugEnabled'];
 
 	const emitter = new Emittery();
 	const target = {};
@@ -978,7 +1126,7 @@ test('mixin() - methodNames must be array of strings or undefined', t => {
 });
 
 test('mixin() - must mixin all methods if no array supplied', t => {
-	const methodsExpected = ['on', 'off', 'once', 'events', 'emit', 'emitSerial', 'onAny', 'anyEvent', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods'];
+	const methodsExpected = ['on', 'off', 'once', 'events', 'emit', 'emitSerial', 'onAny', 'anyEvent', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods', 'logIfDebugEnabled'];
 
 	class TestClass {}
 
@@ -1008,3 +1156,57 @@ test('mixin() - target must be function', t => {
 	t.throws(() => Emittery.mixin('emitter')(undefined));
 	t.throws(() => Emittery.mixin('emitter')({}));
 });
+
+test('isDebug default logger handles symbol event names and object for event data', async t => {
+	const emitter = new Emittery({debug: {name: 'testEmitter', enabled: true}});
+	const eventName = Symbol('test');
+	emitter.on(eventName, () => {});
+	await t.notThrowsAsync(emitter.emit(eventName, {complex: ['data', 'structure', 1]}));
+});
+
+test('isDebug can be turned on globally during runtime', t => {
+	Emittery.isDebugEnabled = true;
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: false,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+
+	emitter.on('test', () => {});
+	emitter.emit('test', 'test data');
+	Emittery.isDebugEnabled = false;
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'emit');
+	t.is(eventStore[2].eventName, 'test');
+	t.is(eventStore[2].debugName, 'testEmitter');
+	t.is(eventStore[2].eventData, 'test data');
+});
+
+test('isDebug can be turned on for and instance without using the constructor', t => {
+	const eventStore = [];
+
+	const emitter = new Emittery({
+		debug: {
+			name: 'testEmitter',
+			enabled: false,
+			logger: (type, debugName, eventName, eventData) => {
+				eventStore.push({type, debugName, eventName, eventData});
+			}
+		}
+	});
+	emitter.debug.enabled = true;
+
+	emitter.on('test', () => {});
+	emitter.emit('test', 'test data');
+	t.true(eventStore.length > 0);
+	t.is(eventStore[2].type, 'emit');
+	t.is(eventStore[2].eventName, 'test');
+	t.is(eventStore[2].debugName, 'testEmitter');
+	t.is(eventStore[2].eventData, 'test data');
+});