Using Puppeteer and Mocha/Sinon to Monitor Google Analytics dataLayer

I am attempting to run automated tests for my Google Analytics interactions using Headless Chrome in combination with Puppeteer, Mocha, and Sinon. However, I am unable to access the dataLayer property for Sinon to monitor its actions. Below is my current implementation, but I’ve encountered an issue where the ‘window’ object is always undefined. ‘test’ is a class that serves as a proxy to forward Puppeteer calls to the browser instance.

const { test } = require('../browser');
const sinon = require('sinon');
const layerName = 'dataLayer';

const assert = sinon.assert;

describe('Analytics Tests', () => {
    let spy;

    it('track homepage analytics', test(async (browser, options) => {
        const pageInstance = await browser.newPage();
        await pageInstance.goto(`${options.appUrl}`);

        spy = sinon.spy(window.dataLayer, 'push');
        assert.called(spy);
        assert.calledWith(spy, [
          'error scenario',
        ]);
        spy.restore();
    }));
});

Here is the code for the Browser class:

const puppeteer = require('puppeteer');

class Browser {
  initialize(done) {
    const puppeteerConfig = this.settings && this.settings.puppeteer ?
      this.settings.puppeteer :
      {};

    puppeteer.launch(puppeteerConfig).then(async (browserInstance) => {
      this.assignBrowser(browserInstance);
      done();
    });
  }

  assignBrowser(instance) {
    this.browser = instance;
    const originalNewPage = this.browser.newPage.bind(this.browser);

    this.browser.newPage = async function () {
      const pageInstance = await originalNewPage();
      this.latestPage = pageInstance;

      return pageInstance;
    };
  }

  configureOptions(settings) {
    this.settings = settings;
  }

  runTest(promise) {
    return (done) => {
      promise(this.browser, this.settings)
        .then(() => done()).catch(done);
    };
  }
}

module.exports = new Proxy(new Browser(), {
  get(target, property) {
    return property in target ? target[property].bind(target) : target.browser[property];
  },
});

hey, try using pageInstance.evaluate(() => window.dataLayer). u cant access window outside page context, so grab your dataLayer within page and then attach your spy. hope that helps fix it.

One approach you might consider is to intercept dataLayer push actions before the page code executes. In my experience, inserting a custom script using page.evaluateOnNewDocument to override dataLayer.push with a custom function helps capture events as they occur. This lets you record calls or trigger any callbacks as needed without having to manually attach spies later. Essentially, by instrumenting the page earlier in its lifecycle, you can avoid the problematic scenario of accessing undefined properties after the fact.

I encountered a similar challenge when testing analytics interactions. My solution was to guarantee that the dataLayer exists and is instrumented before any of your page scripts run. I did this by injecting a script using page.evaluateOnNewDocument that creates a mock dataLayer and wraps the push function. This ensures that you can capture any interactions with dataLayer immediately after page load, even if the window object is not immediately available in your original context. This modification allowed my tests to run reliably in headless mode and provided accurate tracking of events.