diff --git a/plugins/web/opentelemetry-instrumentation-user-interaction/src/instrumentation.ts b/plugins/web/opentelemetry-instrumentation-user-interaction/src/instrumentation.ts index da499ec1a9..cac04f49d5 100644 --- a/plugins/web/opentelemetry-instrumentation-user-interaction/src/instrumentation.ts +++ b/plugins/web/opentelemetry-instrumentation-user-interaction/src/instrumentation.ts @@ -330,6 +330,24 @@ export class UserInteractionInstrumentation extends InstrumentationBase }; } + /** + * Most browser provide event listener api via EventTarget in prototype chain. + * Exception to this is IE 11 which has it on the prototypes closest to EventTarget: + * + * * - has addEventListener in IE + * ** - has addEventListener in all other browsers + * ! - missing in IE + * + * HTMLElement -> Element -> Node * -> EventTarget **! -> Object + * Document -> Node * -> EventTarget **! -> Object + * Window * -> WindowProperties ! -> EventTarget **! -> Object + */ + private _getPatchableEventTargets(): EventTarget[] { + return window.EventTarget + ? [EventTarget.prototype] + : [Node.prototype, Window.prototype]; + } + /** * Patches the history api */ @@ -571,26 +589,27 @@ export class UserInteractionInstrumentation extends InstrumentationBase ); } else { this._zonePatched = false; - if (isWrapped(EventTarget.prototype.addEventListener)) { - this._unwrap(EventTarget.prototype, 'addEventListener'); - api.diag.debug('removing previous patch from method addEventListener'); - } - if (isWrapped(EventTarget.prototype.removeEventListener)) { - this._unwrap(EventTarget.prototype, 'removeEventListener'); - api.diag.debug( - 'removing previous patch from method removeEventListener' + const targets = this._getPatchableEventTargets(); + targets.forEach(target => { + if (isWrapped(target.addEventListener)) { + this._unwrap(target, 'addEventListener'); + api.diag.debug( + 'removing previous patch from method addEventListener' + ); + } + if (isWrapped(target.removeEventListener)) { + this._unwrap(target, 'removeEventListener'); + api.diag.debug( + 'removing previous patch from method removeEventListener' + ); + } + this._wrap(target, 'addEventListener', this._patchAddEventListener()); + this._wrap( + target, + 'removeEventListener', + this._patchRemoveEventListener() ); - } - this._wrap( - EventTarget.prototype, - 'addEventListener', - this._patchAddEventListener() - ); - this._wrap( - EventTarget.prototype, - 'removeEventListener', - this._patchRemoveEventListener() - ); + }); } this._patchHistoryApi(); @@ -619,12 +638,15 @@ export class UserInteractionInstrumentation extends InstrumentationBase this._unwrap(ZoneWithPrototype.prototype, 'cancelTask'); } } else { - if (isWrapped(HTMLElement.prototype.addEventListener)) { - this._unwrap(HTMLElement.prototype, 'addEventListener'); - } - if (isWrapped(HTMLElement.prototype.removeEventListener)) { - this._unwrap(HTMLElement.prototype, 'removeEventListener'); - } + const targets = this._getPatchableEventTargets(); + targets.forEach(target => { + if (isWrapped(target.addEventListener)) { + this._unwrap(target, 'addEventListener'); + } + if (isWrapped(target.removeEventListener)) { + this._unwrap(target, 'removeEventListener'); + } + }); } this._unpatchHistoryApi(); } diff --git a/plugins/web/opentelemetry-instrumentation-user-interaction/test/userInteraction.nozone.test.ts b/plugins/web/opentelemetry-instrumentation-user-interaction/test/userInteraction.nozone.test.ts index 08447543be..38c3cd240b 100644 --- a/plugins/web/opentelemetry-instrumentation-user-interaction/test/userInteraction.nozone.test.ts +++ b/plugins/web/opentelemetry-instrumentation-user-interaction/test/userInteraction.nozone.test.ts @@ -619,5 +619,42 @@ describe('UserInteractionInstrumentation', () => { 'go should be unwrapped' ); }); + + describe('simulate IE', () => { + // Save window.EventTarget reference (including enumerable state) + const EventTargetDesc = Object.getOwnPropertyDescriptor( + window, + 'EventTarget' + )!; + before(() => { + // @ts-expect-error window.EventTarget not optional + delete window.EventTarget; + }); + after(() => { + Object.defineProperty(window, 'EventTarget', EventTargetDesc); + // Undo unwrap putting originals back on it's targets + // @ts-expect-error event listener API not optional + delete Node.prototype.addEventListener; + // @ts-expect-error copy + delete Node.prototype.removeEventListener; + // @ts-expect-error copy + delete Window.prototype.addEventListener; + // @ts-expect-error copy + delete Window.prototype.removeEventListener; + }); + + it('works with missing EventTarget', () => { + /* + * Would already error out with: + * "before each" hook for "works with missing EventTarget" + * ReferenceError: EventTarget is not defined + */ + + fakeInteraction(); + assert.equal(exportSpy.args.length, 1, 'should export one span'); + const spanClick = exportSpy.args[0][0][0]; + assertClickSpan(spanClick); + }); + }); }); });