diff --git a/common/changes/@snowplow/javascript-tracker/issue-1085-snowplow-callback-without-this_2025-01-22-17-05.json b/common/changes/@snowplow/javascript-tracker/issue-1085-snowplow-callback-without-this_2025-01-22-17-05.json new file mode 100644 index 000000000..54c2de540 --- /dev/null +++ b/common/changes/@snowplow/javascript-tracker/issue-1085-snowplow-callback-without-this_2025-01-22-17-05.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@snowplow/javascript-tracker", + "comment": "Allow usage of Snowplow callback without 'this' keyword", + "type": "none" + } + ], + "packageName": "@snowplow/javascript-tracker" +} \ No newline at end of file diff --git a/trackers/javascript-tracker/src/in_queue.ts b/trackers/javascript-tracker/src/in_queue.ts index e9b9fd44f..3b6a8db90 100644 --- a/trackers/javascript-tracker/src/in_queue.ts +++ b/trackers/javascript-tracker/src/in_queue.ts @@ -265,7 +265,15 @@ export function InQueueManager(functionName: string, asyncQueue: Array) // Strip GlobalSnowplowNamespace from ID fnTrackers[tracker.id.replace(`${functionName}_`, '')] = tracker; } - input.apply(fnTrackers, parameterArray); + + // Create a new array from `parameterArray` to avoid mutating the original + const parameterArrayCopy = Array.prototype.slice.call(parameterArray); + + // Add the `fnTrackers` object as the last element of the new array to allow it to be accessed in the callback + // as the final argument, useful in environments that don't support `this` (GTM) + const args = Array.prototype.concat.apply(parameterArrayCopy, [fnTrackers]); + + input.apply(fnTrackers, args); } catch (ex) { LOG.error('Tracker callback failed', ex); } finally { diff --git a/trackers/javascript-tracker/test/unit/in_queue.test.ts b/trackers/javascript-tracker/test/unit/in_queue.test.ts index 7df31124d..34ef8ac5c 100644 --- a/trackers/javascript-tracker/test/unit/in_queue.test.ts +++ b/trackers/javascript-tracker/test/unit/in_queue.test.ts @@ -163,6 +163,34 @@ describe('InQueueManager', () => { asyncQueue.push(['updatePageActivity:firstTracker;secondTracker']); expect(output).toEqual(29); }); +}); + +describe('Snowplow callback', () => { + const asyncQueue = InQueueManager('callback', []); + const mockTrackers: Record = {}; + + let userId: string | null | undefined; + + const newTracker = (trackerId: string): any => { + return { + id: trackerId, + setUserId: function (s?: string | null) { + userId = s; + }, + getUserId: function () { + return userId; + }, + }; + }; + + beforeEach(() => { + const tracker = newTracker('sp1'); + mockTrackers.sp1 = tracker; + }); + + afterEach(() => { + delete mockTrackers.sp1; + }); it('Execute a user-defined custom callback', () => { let callbackExecuted = false; @@ -183,4 +211,37 @@ describe('InQueueManager', () => { ]); }).not.toThrow(); }); + + it('A custom callback with arguments provided will pass those arguments into the callback parameters', () => { + asyncQueue.push([ + function (a: number, b: number) { + expect(a).toEqual(1); + expect(b).toEqual(2); + }, + 1, + 2, + ]); + }); + + it('The callback will be passed the tracker dictionary as the argument if there is only one parameter', () => { + asyncQueue.push([ + function (trackers: Record) { + const tracker = trackers.sp1; + expect(tracker).toEqual(mockTrackers.callback_sp1); + }, + ]); + }); + + it('The callback can access the tracker dictionary using both `this` and the last argument, along with arguments', () => { + asyncQueue.push([ + function (this: any, a: number, b: number, trackers: Record) { + expect(this.sp1).toEqual(mockTrackers.callback_sp1); + expect(a).toEqual(1); + expect(b).toEqual(2); + expect(trackers.sp1).toEqual(mockTrackers.callback_sp1); + }, + 1, + 2, + ]); + }); });