> ## Documentation Index
> Fetch the complete documentation index at: https://docs.allgoodhq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Track custom events on your website

> Send allGood the actions the tag can't see on its own, such as video views, calculator use or chat, using one repeatable pattern. Worked example: Vimeo.

The allGood tag records page views and allGood form submissions on its own. Most of what people actually do on a page is invisible to it: watching a video, using a pricing calculator, chatting with a bot, clicking through an interactive demo.

This guide shows how to send those actions to allGood yourself. It uses one pattern that works for almost anything with JavaScript behind it, then walks through a common case, tracking who watches a video, using a Vimeo player as the example.

## Before you start

* The allGood tag is installed on the page. See [Install the tag](https://docs.allgoodhq.com/mk/developer/web-edge/install-the-tag.md).
* You can edit the page's HTML, or add a Custom HTML tag in Google Tag Manager.
* You've read the short [Identify and track](https://docs.allgoodhq.com/mk/developer/web-edge/identify-and-track.md) reference for the `track()` call.

## What the tag can and can't see

The tag watches the page it's on. It sees the page load, route changes in a single-page app, and submissions on forms you've pointed at allGood.

It can't see two kinds of thing:

* **Anything inside an iframe.** Video players, chat widgets, embedded demos and scheduling tools usually run in an iframe served from another domain. The browser keeps that content separate from your page.
* **Anything that doesn't load a page or submit a form.** Opening a tab, moving a slider in a calculator, or expanding an FAQ changes the page without the tag noticing.

For those, your code tells allGood what happened with `track()`. Each call becomes an activity on the visitor's record, and allGood connects it to the person once they're identified.

For a video player or any other tool, use the pattern below.

## The pattern

Every custom event comes down to four moves:

1. **Find the thing.** Locate the element or widget on the page: an iframe, a button, a calculator's form.
2. **Listen to what it reports.** Most embedded tools publish a JavaScript API with events such as `play`, `message` or `submit`. For your own page elements, use ordinary DOM events like `click` and `change`.
3. **Decide which moments matter.** Don't forward everything. Pick the few moments a marketer would act on, and send each one once.
4. **Send it with `track()`.** Use a clear event name and a consistent set of properties.

The worked example below labels each step with the move it illustrates.

## Worked example: who's watching a video

A marketer wants to know who watched a recording and how far they got. That breaks down into three moments worth sending:

| Event | Sent when | Extra property |
| - | - | - |
| `Video Played` | The viewer first presses play on this page load | — |
| `Video Progressed` | The video reaches 25, 50, 75 and 90%, once each | `percent` |
| `Video Completed` | The video plays to the end | — |

allGood turns each event name into an activity type, so `Video Played` becomes `video.played`. Here's how to build it for a Vimeo player.

**1. Add the queue stub above the tag**

Make sure this line sits **above** the allGood tag:

```html theme={null}
<script>window.agq = window.agq || [];</script>
<script src="https://mk.brand.com/_ag/v1.js" data-ag-key="agsk_..." async></script>
```

The tag loads asynchronously, so your code may run before it's ready. `window.agq` is a queue: anything you push onto it is sent once the tag loads, and pushes after that are sent straight away. You never have to check whether the tag has loaded. This applies to every custom event, not just video.

**2. Load the tool's JavaScript API**

For Vimeo, that's the Player SDK. Add it after the tag, without `async`:

```html theme={null}
<script src="https://player.vimeo.com/api/player.js"></script>
```

Most embedded tools have an equivalent: a script that lets your page talk to the iframe. Look for "JavaScript API", "Player SDK" or "embed API" in the vendor's developer docs.

**3. Find the thing: connect to every player on the page**

Place this script after the iframes, for example just before `</body>`:

```html theme={null}
<script>
  (function () {
    window.agq = window.agq || [];

    document.querySelectorAll('iframe[src*="player.vimeo.com"]').forEach(function (iframe) {
      var player = new Vimeo.Player(iframe);
      // The next steps go here, inside this function.
    });
  })();
</script>
```

Selecting every matching iframe, not just the first one on the page, means three videos are all tracked and a page with none does nothing. Wrapping the code in a function keeps its variables from clashing with other scripts.

**4. Look up the details you'll send with every event**

Ask the player for the video's ID, title and length, once:

```js theme={null}
var video = Promise.all([player.getVideoId(), player.getVideoTitle(), player.getDuration()])
  .then(function (v) {
    return {
      videoProvider: "vimeo",
      videoId: String(v[0]),
      videoTitle: v[1],
      videoDurationSeconds: Math.round(v[2]),
    };
  })
  .catch(function () {
    return { videoProvider: "vimeo" };
  });
```

Many tool APIs, Vimeo's included, return promises: the answer arrives a moment later, so reading it on the next line gets nothing. Storing the promise and waiting on it before each send avoids that bug. The `.catch` keeps events flowing, just without a title, when a video's privacy settings block the lookup.

**5. Send it: write one helper for track()**

```js theme={null}
function send(eventName, extra) {
  video.then(function (details) {
    window.agq.push([
      "track",
      eventName,
      Object.assign({ url: window.location.href }, details, extra),
    ]);
  });
}
```

One helper means every event carries the same properties in the same shape, which is what keeps them usable in reports later. Always include `url`: the tag doesn't know which page a custom event came from unless you send it, and it pulls UTM parameters out of that URL.

**6. Listen, and decide: send Video Played once**

```js theme={null}
var played = false;

player.on("play", function () {
  if (played) return;
  played = true;
  send("Video Played");
});
```

Vimeo fires `play` every time playback starts, including each resume after a pause. That's the "decide which moments matter" move: the `played` flag turns a noisy signal into one meaningful event per page load.

**7. Listen, and decide: send each milestone once**

```js theme={null}
var MILESTONES = [25, 50, 75, 90];
var reached = {};

player.on("timeupdate", function (data) {
  var pct = data.percent * 100;
  MILESTONES.forEach(function (m) {
    if (!reached[m] && pct >= m) {
      reached[m] = true;
      send("Video Progressed", { percent: m });
    }
  });
});
```

`timeupdate` fires about four times a second. Sending all of those would bury the useful signal, so the code sends only when a milestone is first crossed. A viewer who skips ahead past 50% sends 25 and 50 together.

**8. Send Video Completed**

```js theme={null}
player.on("ended", function () {
  send("Video Completed");
});
```

**9. Watch the requests go out**

Open the page with your browser's developer tools on the **Network** tab, then play the video. You'll see a request to your allGood domain shortly after it starts, another at 25%, and a final one at the end.

**10. Confirm on a person's record**

Submit a form on the same page with a test email, then find that person in allGood.

When it's working, you'll see `Video Played`, a `Video Progressed` for each milestone passed, and `Video Completed` on the person's timeline, each with the video's title. Views from before the form fill appear too, because allGood connects earlier anonymous activity to the person once they identify themselves.

### The complete Vimeo script

```html theme={null}
<script>window.agq = window.agq || [];</script>
<script src="https://mk.brand.com/_ag/v1.js" data-ag-key="agsk_..." async></script>
<script src="https://player.vimeo.com/api/player.js"></script>

<!-- your Vimeo iframes -->

<script>
  (function () {
    var MILESTONES = [25, 50, 75, 90];
    window.agq = window.agq || [];

    document.querySelectorAll('iframe[src*="player.vimeo.com"]').forEach(function (iframe) {
      var player = new Vimeo.Player(iframe);
      var played = false;
      var reached = {};

      var video = Promise.all([player.getVideoId(), player.getVideoTitle(), player.getDuration()])
        .then(function (v) {
          return {
            videoProvider: "vimeo",
            videoId: String(v[0]),
            videoTitle: v[1],
            videoDurationSeconds: Math.round(v[2]),
          };
        })
        .catch(function () {
          return { videoProvider: "vimeo" };
        });

      function send(eventName, extra) {
        video.then(function (details) {
          window.agq.push([
            "track",
            eventName,
            Object.assign({ url: window.location.href }, details, extra),
          ]);
        });
      }

      player.on("play", function () {
        if (played) return;
        played = true;
        send("Video Played");
      });

      player.on("timeupdate", function (data) {
        var pct = data.percent * 100;
        MILESTONES.forEach(function (m) {
          if (!reached[m] && pct >= m) {
            reached[m] = true;
            send("Video Progressed", { percent: m });
          }
        });
      });

      player.on("ended", function () {
        send("Video Completed");
      });
    });
  })();
</script>
```

## Apply the pattern to other tools

The four moves stay the same. Only what you listen to changes.

| What you want to know | Listen to | Suggested event |
| - | - | - |
| Who watches video in another player | The player's JavaScript API: play, progress or time, and end events | `Video Played`, `Video Progressed`, `Video Completed`, with `videoProvider` set to the player |
| Who uses your pricing calculator | The calculator's `change` or "calculate" button `click` | `Pricing Calculator Used`, with the inputs that matter, such as `seats` |
| Who talks to your chat bot | The chat vendor's JavaScript events, if it publishes them | `Chat Started`, `Meeting Booked` |
| Who reads to the end of a long page | Scroll position, checked with an `IntersectionObserver` on a marker near the end | `Page Scrolled`, with `percent` |
| Who opens a key section | The `click` on a tab or accordion | `Section Opened`, with `section` |

Each row works the same way as the video example: find it, listen, keep only the moments that matter, and send them through one helper.

## Name events so they stay useful

* **Use `Object Verbed`, in title case:** `Video Played`, `Demo Requested`. This matches the convention in [Identify and track](https://docs.allgoodhq.com/mk/developer/web-edge/identify-and-track.md).
* **Keep one name per kind of moment,** and put the details in properties. `Video Progressed` with `percent: 50` beats `Video 50 Percent`, which becomes a separate activity type for every milestone.
* **Reuse names across tools.** If two players both send `Video Played` with a `videoProvider` property, one report covers both.
* **Don't put personal data in properties or URLs.** Identify people with `identify()` or a form, not by stuffing an email into an event.

## Replacing a Munchkin snippet

Munchkin had no custom events, so pages that tracked these actions for Marketo usually faked a page visit, such as `visitWebPage('/video/76979871', 'video=finished')`. Remove those calls and send real events instead. Any Marketo Smart Campaign that filtered on the made-up URLs needs rebuilding against the new activities.

## Troubleshooting

**Nothing reaches allGood.** Check **Send events to allGood** in Web Tracking settings first. If it's on, check consent: custom events wait until your consent platform answers, and are dropped on a no. See [Two settings can silence these](https://docs.allgoodhq.com/mk/developer/web-edge/identify-and-track.md#two-settings-can-silence-these).

**The tool's object is undefined, such as `Vimeo is not defined`.** Its API script hadn't loaded when your code ran. Load it without `async`, above your script.

**Events arrive without details.** The tool refused a lookup, often because of privacy or embed settings on the vendor's side. Check those settings.

**Events arrive twice.** The script is on the page twice, often once in the page and once in GTM. Keep one.

If you're still not seeing events after working through these, contact allGood support or your allGood team, and include the page URL and a screenshot of the Network tab after the action.

## Related articles

* [Identify and track](https://docs.allgoodhq.com/mk/developer/web-edge/identify-and-track.md): the `track()` and `identify()` reference
* [Install the tag](https://docs.allgoodhq.com/mk/developer/web-edge/install-the-tag.md)
* [What consent gates](https://docs.allgoodhq.com/mk/developer/web-edge/consent-behavior.md)
* [Test your setup](https://docs.allgoodhq.com/mk/developer/web-edge/test-your-setup.md)
* [Vimeo Player SDK](https://github.com/vimeo/player.js)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.