analytics.js icon indicating copy to clipboard operation
analytics.js copied to clipboard

TypeError: window.heap.push is not a function.

Open JustinTRoss opened this issue 6 years ago • 16 comments

If loading Heap separately from Segment, when identifying with Heap (window.heap.identify), the page crashes with error: TypeError: window.heap.push is not a function.

While there's the obvious option of deferring all Heap usage to Segment, it seems like a bug to force the user down that path and doubly so to crash the page ambiguously.

Thoughts?

JustinTRoss avatar May 22 '19 21:05 JustinTRoss

@JustinTRoss did you ever figure out a way around this. I was hoping this would work:

window.heap && window.heap.identify(data.name);

Can't seem to get around this error

Sbphillips19 avatar Mar 25 '20 22:03 Sbphillips19

I can't recall exactly what we did. My guess is that we ended up abandoning segment.

If I'm recalling correctly, your proposal wouldn't work because segment actually rewrote the heap object somehow. window.heap.identify still exists, but window.heap.push does not. You could of course condition function call on existence of window.heap.push, but you would likely then just not identify with heap or not identify reliably, the partial data from which would likely be worthless.

JustinTRoss avatar Mar 25 '20 22:03 JustinTRoss

@JustinTRoss just to be clear what do you mean by abandoning segment. Did you just get rid of using heap.io?

I tried doing this as well:

window.heap.loaded && window.heap.identify(data.name);

But it then just doesn't load the page. I feel like maybe this is an issue with compatibility with react? I just don't get why window.heap.push doesn't exist. Been just trying anything I can think of.

Sbphillips19 avatar Mar 26 '20 00:03 Sbphillips19

I was using Heap at the time, and I was trying out Segment. I believe I resolved to abandon Segment and just keep using Heap as I had been. I'm sure an equally appropriate option would have been to switch fully to Segment and stick to only the features they expose. Here is their instruction on Heap identification: https://segment.com/docs/connections/destinations/catalog/heap/#identify. If I recall correctly, it's slightly restrictive in functionality.

Whatever is happening, window.heap as initialized by Segment renders parts of native Heap functionality unusable, presumably by actually mutating the Heap object's api.

JustinTRoss avatar Mar 26 '20 00:03 JustinTRoss

@JustinTRoss so actually realized this issue and it was extremely stupid/ maybe similar to what you were doing (loading 2 libraries)- but our marketing guy added in heap to GTM so it was getting loaded twice. He added it in because we are using webflow for the main page. I had no idea it was added there as I didn't see any code in the webapp outside the script. Basically it was getting loaded twice/ causing issues. Stupid error, but maybe will help someone else if they read this

Sbphillips19 avatar Mar 26 '20 00:03 Sbphillips19

I'm coming from google and I will try explain how this error occurs and how to avoid it.

It's starting with the default Heap snippet code that can be found in Heap's documentation Installation Guides:

<script type="text/javascript">
  window.heap=window.heap||[],heap.load=function(e,t){window.heap.appid=e,window.heap.config=t=t||{};var r=document.createElement("script");r.type="text/javascript",r.async=!0,r.src="https://cdn.heapanalytics.com/js/heap-"+e+".js";var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(r,a);for(var n=function(e){return function(){heap.push([e].concat(Array.prototype.slice.call(arguments,0)))}},p=["addEventProperties","addUserProperties","clearEventProperties","identify","resetIdentity","removeEventProperty","setEventProperties","track","unsetEventProperty"],o=0;o<p.length;o++)heap[p[o]]=n(p[o])};
  heap.load("YOUR_APP_ID");
</script>

which can be easily translated to much more readable version below

window.heap = window.heap || []

heap.load = function (appId, heapConfig) {
    window.heap.appid = appId;
    window.heap.config = heapConfig || {};

    const script = document.createElement("script");
    script.type = "text/javascript";
    script.async = true;
    script.src = "https://cdn.heapanalytics.com/js/heap-" + appId + ".js";
    
    const firstScript = document.getElementsByTagName("script")[0];
    firstScript.parentNode.insertBefore(script, firstScript);
    
    const cloneArray = (arrayLike) => Array.prototype.slice.call(arrayLike, 0);

    const createMethod = function (method) {
        return function () {
            heap.push([
                method,
                ...cloneArray(arguments)
            ]);
        }
    };

    const methods = [
        'addEventProperties',
        'addUserProperties',
        'clearEventProperties',
        'identify',
        'resetIdentity',
        'removeEventProperty',
        'setEventProperties',
        'track',
        'unsetEventProperty',
    ];
    
    for (let method of methods) {
        heap[method] = createMethod(method)
    }
};

heap.load("YOUR_APP_ID");

The problem is that the snippet from the Heap defines window.heap as an array

window.heap = window.heap || []

but when the heap script defined here

script.src = "https://cdn.heapanalytics.com/js/heap-" + appId + ".js";

is fully loaded, window.heap is NOT an array anymore, now it's an object.

The snippet also create methods that access global heap variable by using heap.push

var n = function (e) {
  return function () {
    heap.push([e].concat(Array.prototype.slice.call(arguments, 0)))
  }
}

When this problem may occurs?

When you pass window.heap as a function argument like below

function trackUser(heap, user) {
  heap.identify(user.id);
}

// ...
trackUser(window.heap, authenticatedUser);

especially when you do also some async stuff like

async function trackUser(heap, getUser) {
  const user = await getUser(); // <------- Because it's async, heap script may be already loaded and now it's an object

  if (user) {
     // Code below will execute heap.push which throws an error...
     // because global heap is the object now, not an array.
     heap.identify(user.id);
  }
}

// ...
trackUser(window.heap); // Here is passed an array version of window.heap from the code snippet

How to fix this error?

Don't pass heap as a function argument or pass a function that returns window.heap.

Avoid

trackUser(window.heap, authenticatedUser);

Better

trackUser(() => window.heap, authenticatedUser);
async function trackUser(getHeap, getUser) {
  const user = await getUser();

  if (user) {
     const heap = getHeap();
     heap.identify(user.id);
  }
}

// ...
trackUser(getHeap, getUser);

or just use window.heap directly but then code testing is a bit more harder...

async function trackUser(getUser) {
  const user = await getUser();

  if (user) {
     window.heap.identify(user.id);
  }
}

// ...
trackUser(getUser);

I hope that it will help someone.

heap-explained

hinok avatar Mar 28 '20 15:03 hinok

Thanks for the detailed explanation @hinok ! It seems like it could be a functional workaround. Ideally, Segment would just stop mutating other libs, so I'm going to leave this open in hope that a root resolution might one day be found.

JustinTRoss avatar Mar 28 '20 16:03 JustinTRoss

Why was this closed @pooyaj? I'm seeing the issue actively.

quantizor avatar Jul 31 '20 16:07 quantizor

@probablyup do you have a live website or a sandbox with the issue for us to debug?

pooyaj avatar Jul 31 '20 22:07 pooyaj

The error is in a protected part of our website, so I can't link. The reproduction though is loading the heap client via segment and then calling heap.identify() which internally uses heap.push which segment I guess overwrites, or possibly an old version of the client is loaded by segment.

quantizor avatar Jul 31 '20 23:07 quantizor

👍 Let us repro, and get back!

pooyaj avatar Jul 31 '20 23:07 pooyaj

@probablyup @pooyaj We don't use segment in our application but we use heap and we've had exactly the same issue. The root of the problem is the way how heap instance is created in the official code snippet before the heap script is fully loaded. The problem appears always when you pass a reference of heap before the script is loaded.

Briefly looking at https://github.com/segmentio/analytics.js-integrations/blob/master/integrations/heap/lib/index.js#L35 it seems that the same problem is in this integration.

Script is NOT loaded: it's just an array with methods Script is fully loaded: it's an object (doesn't have anymore .push and array in prototype chain)

You can try and recreate a demo using code from my comment https://github.com/segmentio/analytics.js/issues/605#issuecomment-605464507

hinok avatar Aug 01 '20 10:08 hinok

I think I got a solution for this (using react and heap) The problem is that if you look at the dev tools, it's possible you are calling several times heap. You can notice if you have several scripts loaded. image

My solution using react, and a persisiting layout through route changes, is to load heap just once using a function like this:

export function loadAndSetScript(innerHtml, position, id) {
  if (!position) {
    return;
  }

  const script = document.createElement('script');
  script.setAttribute('async', '');
  script.setAttribute('id', id);
  script.type = 'text/javascript';
  script.innerHTML = innerHtml;
  position.appendChild(script);
}

then calling this fucntion in your persisiting layout component, you can do something like this:

import React, { useRef } from 'react';
const PersistingLayout = ({ prop1, prop2 }) => {
....
....
  const loaded = useRef();
  if (typeof window !== 'undefined' && !loaded.current) {
    if (!document.querySelector('#heap')) {
      loadAndSetScript(
        `window.heap=window.heap||[],heap.load=function(e,t){window.heap.appid=e,window.heap.config=t=t||{};var r=document.createElement("script");r.type="text/javascript",r.async=!0,r.src="https://cdn.heapanalytics.com/js/heap-"+e+".js";var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(r,a);for(var n=function(e){return function(){heap.push([e].concat(Array.prototype.slice.call(arguments,0)))}},p=["addEventProperties","addUserProperties","clearEventProperties","identify","resetIdentity","removeEventProperty","setEventProperties","track","unsetEventProperty"],o=0;o<p.length;o++)heap[p[o]]=n(p[o])};
  heap.load(`${yourHeapNumber}`);`,
        document.querySelector('head'),
        'heap'
      );
    }
    loaded.current = true;
  }
.....
......

JavierPiedra avatar Sep 10 '20 03:09 JavierPiedra

Thanks for the detailed explanation @hinok ! It seems like it could be a functional workaround. Ideally, Segment would just stop mutating other libs, so I'm going to leave this open in hope that a root resolution might one day be found.

The problem I found is that heap is being called different times. This solves it.

JavierPiedra avatar Sep 11 '20 14:09 JavierPiedra

@Sbphillips19 - 1000 thank you's to you! I've been trying to use heap to track scrolling. Your note about heap being loaded twice because marketing added it in GTM is exactly what happened to me, i've been trying to get this to work consistently for a few hours, and the loading issue was causing it to raise the exception "heap.push is not a function" about 95% of the time. The 5% of the time it worked was really throwing me off course! Anyway, i removed the heap snippet and then was able to track scrolling.

richardtemple29 avatar Sep 17 '20 19:09 richardtemple29

Just to add to this thread:

One potential reason you may be seeing this error and having a hard time debugging it:

The initializing script is being run somewhere else.

For example, if you're adding it to Google Tag Manager and it already exists in your frontend codebase (or vice versa).

To check, just run heap in the DevTools console of your browser on your page.

alecbw avatar Nov 27 '21 20:11 alecbw