Skip to content

Commit 445aa1a

Browse files
committed
Ensure we always call the underlying method & retain this
1 parent 4891bcf commit 445aa1a

1 file changed

Lines changed: 48 additions & 27 deletions

File tree

plugins/3rd-party-optimizer/src/yieldGTMCalls.ts

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* eslint-disable @typescript-eslint/no-unnecessary-condition,@typescript-eslint/no-unsafe-function-type,@typescript-eslint/no-empty-function */
1+
/* eslint-disable @typescript-eslint/no-unnecessary-condition,@typescript-eslint/no-unsafe-function-type */
22
"use strict"
33

44
// turns on override logs
@@ -322,14 +322,15 @@ document.addEventListener("readystatechange", () => {
322322
})
323323

324324
// #region GTM override
325-
function wrapDataLayerPush(push: DataLayer["push"], dataLayer: DataLayer) {
325+
function wrapDataLayerPush(push: DataLayer["push"]) {
326326
// Must be a (non-async) function, not an arrow function, because we need to bind the original `this` context
327327
// the GTM dataLayer push function returns `true` if the push was successful, we just assume it is.
328328
// The `...args` spread is needed so the call results in the exactly same result as the original.
329-
return function yieldingPush(...args: object[]) {
330-
void yieldUnlessUrgent(true).then(function innerPush() {
331-
// In case we override the native Array#push here, we need to set the original array as `this` to not cause runtime errors
332-
push.apply(dataLayer, args)
329+
// The function syntax is important here so we keep the correct `this`.
330+
return function yieldingPush(this: DataLayer, ...args: object[]) {
331+
void yieldUnlessUrgent(true).then(() => {
332+
// The arrow FN is important here so we keep the correct `this`.
333+
push.apply(this, args)
333334
})
334335
return true
335336
}
@@ -353,7 +354,7 @@ function defineCustomDataLayerPush(dataLayer: DataLayer) {
353354
if (DEBUG) console.log("set dataLayer.push", value)
354355

355356
if (value === mostRecentPushWrapper) return // skip for `window.dataLayer = window.dataLayer||[]`
356-
mostRecentPushWrapper = value ? wrapDataLayerPush(value as DataLayer["push"], dataLayer) : undefined
357+
mostRecentPushWrapper = value ? wrapDataLayerPush(value as DataLayer["push"]) : undefined
357358
},
358359
})
359360
Object.defineProperty(dataLayer, "__f", {
@@ -403,40 +404,60 @@ const gtmObserver = new MutationObserver(() => {
403404
gtmObserver.observe(document.documentElement, { childList: true, subtree: true })
404405

405406
// #region History/submit wrapper override
406-
function wrapListener<T extends object>(target: T, method: keyof T, value: Function) {
407-
// @ts-expect-error TS(2339): Prototype chain call. We try __proto__ first, as this will usually be the original method.
408-
const originalMethod: Function = (target.__proto__ as unknown as T)[method] ?? (target[method] as Function)
407+
function callOriginalMethod(
408+
this: unknown,
409+
originalMethod: Function,
410+
args: unknown[],
411+
callIfFirstArgIsntObject = false
412+
) {
413+
const firstArg = args[0] ?? this
414+
415+
const argIsObject = firstArg != null && typeof firstArg === "object"
416+
417+
if (argIsObject && !("__f" in firstArg)) {
418+
originalMethod.apply(this, args)
419+
420+
// @ts-expect-error TS(2339): Flag to indicate that the native method was called
421+
firstArg.__f = true
422+
} else if (!argIsObject && callIfFirstArgIsntObject) {
423+
// If for some reason, we haven't called the original method yet, we call it here.
424+
originalMethod.apply(this, args)
425+
}
426+
}
409427

410-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
411-
return (...args: [data: object, ...args: any[]]) => {
428+
function wrapListener(originalMethod: Function, value: Function) {
429+
// the function syntax is important here so we keep the correct `this`.
430+
return function yieldingListener(this: unknown, ...args: [data: object, ...args: unknown[]]) {
412431
if (DEBUG) {
413-
console.log("Yielding for", method)
414-
console.timeStamp(method as string)
432+
console.log("Yielding for", originalMethod)
433+
console.timeStamp(originalMethod as unknown as string)
415434
}
416435

417436
// We first call the original: This optimizes for UX & correctness of React components.
418437
// e.g., for pushState, when a component renders on a new route, it might set state and/or read from the URL. If the URL isn't
419438
// accurate, it might lead to wrong behavior.
420439
// We don't want to call the underlying native method twice (or multiple times), so we add a
421-
// flag to the data object.
422-
if (typeof args[0] === "object" && !("__f" in args[0])) {
423-
originalMethod.apply(target, args)
424-
425-
// @ts-expect-error TS(2339): Flag to indicate that the native method was called
426-
args[0].__f = true
427-
}
440+
// flag to the data object or `this`.
441+
callOriginalMethod.call(this, originalMethod, args)
428442

429443
// If `method` is overriden N times, it creates N yield points (as overrides might be chained)
430444
void yieldUnlessUrgent().then(() => {
431-
value.apply(target, args)
445+
// The arrow FN is important here so we keep the correct `this`.
446+
value.apply(this, args)
432447
})
433448
}
434449
}
435450
function overrideListener<T extends object>(target: T, method: keyof T) {
436-
// The initial value is a noop, so that the first override doesn't call the native method.
437-
// We still need to set it to a function, so that the `get` function returns a function that is
438-
// used if nothing ever overrides it.
439-
let mostRecentWrapper: Function | undefined = wrapListener(target, method, () => {})
451+
// @ts-expect-error TS(2339): Prototype chain call. We try __proto__ first, as this will usually be the original method.
452+
const originalMethod: Function = (target.__proto__ as unknown as T)[method] ?? (target[method] as Function)
453+
454+
let mostRecentWrapper: Function | undefined = wrapListener(
455+
originalMethod,
456+
// The function syntax is important here so we keep the correct `this`.
457+
function firstOverride(this: unknown, ...args: [data: object, ...args: unknown[]]) {
458+
callOriginalMethod.call(this, originalMethod, args, true)
459+
}
460+
)
440461

441462
Object.defineProperty(target, method, {
442463
enumerable: true,
@@ -447,7 +468,7 @@ function overrideListener<T extends object>(target: T, method: keyof T) {
447468
if (DEBUG) console.log(`set ${String(method)}`, target, value)
448469

449470
if (value === mostRecentWrapper) return
450-
mostRecentWrapper = value ? wrapListener(target, method, value as Function) : undefined
471+
mostRecentWrapper = value ? wrapListener(originalMethod, value as Function) : undefined
451472
},
452473
})
453474
}

0 commit comments

Comments
 (0)