Browser.application doesn't work with popular Analytics approaches

We recently moved a production B2B app from Elm 18 to 19. What we discovered unfortunately after the fact, was that Browser.application’s behavior & timing of taking over the tag doesn’t play well with popular analytics injection approaches.

Our HTML loads two separate JS bundles. The first is our app code, which in turn triggers a parallelized request to Segment.com for a combined analytics JS bundle. We stall the loading of our Elm app until the Segment bundle has made some progress, so that we can initialize our Elm app with the real-ID or anonymous ID of the current user.

We additionally use Segment to load Intercom (live chat floating widget) and it does so through a few iFrame tricks eventually resulting in a child-of-body div tag representing the floating widget.

The problem is that that either the iFrame or the div for Intercom get blown away once Elm takes over the tag as a result of using Browser.application.

My industry-take is it’s going to be hard (impossible?) to work w/ Elm in full Browser.application mode because so many of these ecosystem analytics/widgets/etc need to store their work in a private child-of-body DOM node.

I think I understand the rationale for Elm so completely taking over, but I’d hope there might be another best-of-all-worlds where Browser.application can leave nodes it didn’t generate alone, and if another “Full browser app framework” modifies URL out of band, perhaps Elm can warn/complain in the manner it does when someone uses the href attribute with JS in it (it triggers an XSS alert)

6 Likes

We also use Segment and load Intercom and use Browser.application, and somehow it all works.

We load Elm by passing a node:

const node = document.getElementById("app")
const app = ElmApp.Elm.Main.init({ node, flags })
1 Like

This is essentially our exact set up as well for those lines - how/when do you load Segment? Ahead of this or do you port out to trigger it? (Just to front load more info: we load it ahead of our Elm code so we can pass in the ID as one of those flags. I suspect I could rewire it to be triggered after an initial render and make things work).

This is a hacky way, but if you know what it will create in the body you can add an empty element on the Elm side so the virtual-dom doesn’t touch it when diffing, a keyed node also works.

For instance, if you know that a library will inject a div with the ID mydiv and some iframe, you can render your Document as

[ appView model
, div [ id "mydiv" ] []
]

And the library is free to add stuff in #mydiv without Elm touching it.

1 Like

Thanks for the suggestion; I think that’ll work for the div which I do know the ID of, will explore if there is a stable way to preserve the iframe, which is the first thing being discarded that I need to preserve (I know the code creates it via document.createElement(“iframe”) so I may be out of luck)

In talking a little more with @pateh, he suggested something like a Browser.elementApplication - that would be essentially the solution I am looking for

2 Likes

I think Browser.application ignores node field in the object you pass to init. It takes over the whole body and that’s what @Aaron_White is talking about. It can be mitigated by delaying initialisation of external libraries until after application is rendered. Elm seems keeping DOM nodes which it did not create after first render, but I don’t know how stable solution it is.

To test if Browser.application ignores node field I tried the following code. Please note, I am not appending the node to document but my application still renders!

const node = document.createElement(‘div’);
node.id = ‘app-root’;

const flags = {
initialSeed: Math.floor(Math.random() * 0xFFFFFFFF),
};
this.app = Elm.Main.init({node, flags});

@szubtsovskiy is correct - it was unexpected to both take the node and take over even more! For those interested, @pateh helped me concoct a hack. In my index.html, I create a <div id="elmIsolationContainer"><div id="nodeToPassInit"/></div> and I have my build process (gulp) do a replace on the pre-compressed bundle’s Elm Virtual DOM implementation: var bodyNode = _VirtualDom_doc.body to var bodyNode = _VirtualDom_doc.getElementById("elmIsolationContainer"). It’s not the prettiest option, but for the moment the one I need.

2 Likes

@Aaron_White We load segment as the first thing in the body. Then at the end of the body we load our app bundle. Early on when migrating to 0.19 I tested this as I was having this concern and it just worked. I have no idea why this work for us, would like to know though.

Just for comparison’s sake, our Segment loads GA, Mixpanel, Hubspot and Fullstory just fine. However, the inclusion of Intercom specifically highlights the undesirable child-of-body interaction(s)

I changed our init to (removed the node):

const app = ElmApp.Elm.Main.init({ flags })

And the Intercom widget doesn’t load this way. But with node is shows fine.

I have been testing a bit more. And is not as clear what is going on.
In our case when we have node there is a high chance that the widget will load, but sometimes doesn’t. When I don’t have node there is a high chance that it will not load, but sometimes it does. I understand too that node is being ignored. But somehow it does make a difference apparently.

So this is something else to do with the order of things being loaded.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.