Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

self-describing events on form submit are not working #1151

Open
DeCryptinWeb opened this issue Feb 7, 2023 · 3 comments
Open

self-describing events on form submit are not working #1151

DeCryptinWeb opened this issue Feb 7, 2023 · 3 comments
Labels
type:defect Bugs or weaknesses. The issue has to contain steps to reproduce.

Comments

@DeCryptinWeb
Copy link

Hi

I am having a problem with self-describing events in the browser. In my case, I'm trying to send an event when my form is submitted.

My first attempt was:

import { trackSelfDescribingEvent } from "@snowplow/browser-tracker";
import { data, schema } from "./assets"
 

export const handleFormSubmit = () => {
  const form = document.querySelector(".my-form");
  
  form.addEventListener("submit", () => {	
    trackSelfDescribingEvent({
      event: {
        data,
        schema,
      },
    });
  });
}

My second attempt was to control the form submission (of course it didn’t change anything):

import { trackSelfDescribingEvent } from "@snowplow/browser-tracker";
import { data, schema } from "./assets"
 

export const handleFormSubmit = () => {
  const form = document.querySelector(".my-form");
  
  form.addEventListener("submit", (e) => {
    e.preventDefault(); // <-- added this ...
  
    trackSelfDescribingEvent({
      event: {
        data,
        schema,
      },
    });
  
    form.submit(); // <--  ... and that
  });
}

In Chromium Browsers both examples work in most of the cases fine, but in Firefox and Safari both work only in around 30% of the cases. I suspect this to happen, because there is a race condition (or something similar) between the execution of the event/request and the site refresh after submitting the form. So I ended up doing this:

import { trackSelfDescribingEvent } from "@snowplow/browser-tracker";
import { data } from "./data"
 

export const handleFormSubmit = () => {
  const form = document.querySelector(".my-form");
  
  form.addEventListener("submit", (e) => {
    e.preventDefault();
  
    trackSelfDescribingEvent({
      event: {
        data,
        schema: "iglu:ch.digitecgalaxus/login_action/jsonschema/1-0-0",
      },
    });
  
    setTimeout(() => form.submit(), 1000); // <-- 🙄
  });
}

Using Google Tag Manager, you can pass a callback function to your events, which is executed only after the event/request has been sent. A callback function would look like this:

import { trackSelfDescribingEvent } from "@snowplow/browser-tracker";

export const handleFormSubmit = () => {
  const form = document.querySelector(".my-form");
  
  form.addEventListener("submit", (e) => {
    e.preventDefault();
  
    window.dataLayer.push({
      event : "processLink",
      eventCallback : () => form.submit(), // <-- ❤️❤️❤️
      eventTimeout : 500
    });
  });
}

From what I understand, Snowplow doesn't offer anything like that, right?

After digging a bit deeper inside the snowplow documentation, I found @snowplow/browser-plugin-form-tracking and tried it out. According to what I read in the plugin documentation, the plugin will capture the submit event correctly only if the event is not prevented manually (by using event.preventDefault()). So I included the plugin in the snowplow configuration object and basically retried to run the same code as on my very first example described above:

import { ClientHintsPlugin } from "@snowplow/browser-plugin-client-hints";
import { FormTrackingPlugin, enableFormTracking } from "@snowplow/browser-plugin-form-tracking";

...
let config = {
    appId,
    bufferSize,
    connectionTimeout: 5000,
    ...
    plugins: [ClientHintsPlugin(), FormTrackingPlugin()], // <-- 
    ...
  };
...

enableFormTracking(); // <-- 
import { trackSelfDescribingEvent } from "@snowplow/browser-tracker";
import { data, schema } from "./assets"
 
export const handleFormSubmit = () => {
  const form = document.querySelector(".my-form");
  
  form.addEventListener("submit", () => {
    trackSelfDescribingEvent({
      event: {
        data,
        schema
      },
    });
  });
}

As a result of running the version that includes the plugin, I spotted two new events (focus_form and change_form) in Safari and Firefox. But unfortunately the submit_form event is still missing.

As a consequence, I must stick with the setTimeout solution for now, which is disappointing.

I would appreciate it if you could help me to find a better solution.

Thanks

Details

Browsers: Firefox 109.0.1 (64-bit) and Safari Version 15.6.1 (17613.3.9.1.16)
OS: mac OS Monerey Version 12.5.1

@DeCryptinWeb DeCryptinWeb added the type:defect Bugs or weaknesses. The issue has to contain steps to reproduce. label Feb 7, 2023
@jethron
Copy link
Contributor

jethron commented Feb 15, 2023

Short answer: Not at the moment. Your first snippet should still work, as long as the form's submission page ends up on the same origin, the event that wasn't able to send should be sent on the next page when the next event attempts to send. You can also try with eventMethod: beacon and cross your fingers.

Long answer: The closest API for an after-event callback is via an inline plugin (basically the v3 equivalent of #30), something like:

import { trackSelfDescribingEvent, addPlugin } from "@snowplow/browser-tracker";
import { data, schema } from "./assets"
 
export const handleFormSubmit = () => {
  const form = document.querySelector(".my-form");
  
  form.addEventListener("submit", (e) => {
    e.preventDefault(); // <-- added this ...
  
    const eventWaiter = new Promise((res, rej) => {
      addPlugin({plugin: {afterTrack: res}});
      setTimeout(rej, 2000);
    });
    trackSelfDescribingEvent({
      event: {
        data,
        schema,
      },
    });
    eventWaiter.finally(() => form.submit());
  });
}

Or slightly simpler and less promisy:

import { trackSelfDescribingEvent, addPlugin } from "@snowplow/browser-tracker";
import { data, schema } from "./assets"
 
export const handleFormSubmit = () => {
  const form = document.querySelector(".my-form");
  
  form.addEventListener("submit", (e) => {
    e.preventDefault(); // <-- added this ...
  
    const submitter = () => form.submit();
    addPlugin({plugin: {afterTrack: submitter}});

    trackSelfDescribingEvent({
      event: {
        data,
        schema,
      },
    });

    setTimeout(submitter, 2000);
  });
}

(NB: If you're generalizing this to not call immediately after you add the plugin, there's a potential race here where if somehow another event fires first (e.g. a page ping) you might resolve early. Shouldn't matter for the above since they're immediate, but the afterTrack function will receive a copy of the event which you can inspect to verify it is in fact your form_submit or whatever event you're looking for before you decide to resolve, modify it as appropriate if desired.)

Unfortunately, it doesn't work for this use case. Since the default eventMethod is post and the browser-tracker has no public API for queuing a synchronous XMLHttpRequest, this basically gets fired immediately so it happens before the event request actually goes anywhere (the actual XMLHttpRequest processing happens in a new task so this "afterTrack" will pretty much always fire before the request even gets made, let alone returns a response). browser-tracker uses XHR for both GET and POST requests so eventMethod: 'get' won't work either, same issue. It doesn't look like you can force it to fall back to an Image.src get request on any modern browsers. (I suspect executeQueue() needs yet another toggle to check in addition to useXHR which is determined by browser features, this part might actually be a bug?) But even if you modify the library to allow that, you end up with good-old "browser kills your request as it navigates away from the page" behaviour instead.

So to get around all of that, you have to switch to eventMethod: 'beacon' and hope it works better for you and you don't experience any of the other weirdness associated with that API. :)
#1087 is somewhat related to this request, but for the node tracker, trying to solve a similar problem.

In better news, even without the timeout, your original code (event without e.preventDefault()) should successfully be storing the event in localStorage, so even though the event is not sent straight away on the form page, if the destination page is on the same origin it should "re-send" the event anyway, so you still collect it. If not, that is probably a bug.

@davidher-mann
Copy link

Thanks for your feedback jethron, very helpful. Regarding eventMethod: 'beacon': according to my tests the problem with beacon is, that ad blocker like uBlock origin are blocking all requests send via beacon, since they know that the intent of using beacon is mostly analytics. Anyway, we’ll check your feedback and keep you posted.

@jethron
Copy link
Contributor

jethron commented Feb 23, 2023

Another thought, though it's not very elegant: you could also set up a PerformanceObserver.

new PerformanceObserver((list, po) => {
  const eventReq = list.getEntries().find((entry) => entry.name.endsWith("com.snowplowanalytics.snowplow/tp2"));
  if (eventReq && eventReq.responseEnd) {
    po.disconnect();
    form.submit();
  }
}).observe({ entryTypes: ["resource"] });

If the request never happens it'll still wait forever so you'll want the timeout as a backup, but should work otherwise.
Should have reasonable browser support except for Internet Explorer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:defect Bugs or weaknesses. The issue has to contain steps to reproduce.
Projects
None yet
Development

No branches or pull requests

3 participants