Tracking Interactions

Now that the Tracker is up and running we can start thinking about Tracking some Elements or Components as LocationContexts using Tracked Elements or Tracked Contexts HOCs.

The former follow HTML semantic, while the latter are HOCs that can render any Component or Element. Both are built on top of our Providers and Location Wrappers.

Locations?

Take a look at the Locations Introduction for more information on what Locations are and why are they so important.

Interactions

A good rule of thumb is to start by identifying all the interactions in the Application.

Often these will fall in three categories:

  • Elements & Element-like Components
  • Custom Components
  • Third Party Components

Elements & Element-like Components

These are the easiest to track.

React SDK comes with Tracked Elements and Tracked Contexts Components which mimic regular HTML elements or elements that provide a similar interface. Tracking them is just a matter of swapping the original tags/components with the tracked versions of them.

Here are some examples around Buttons and Anchors. The same approach can be applied to any HTML Element.

Pressable

Anything that the user can interact with, but does not cause a URL change, can be considered Pressable.

Here are some common examples of Pressable:

A button Element can be tracked by simply swapping the original Element tag with TrackedButton.
import { TrackedButton } from '@objectiv/tracker-react';

<button onClick={handleClick}>
Click Me!
</button>

// Tracked equivalent

<TrackedButton onClick={handleClick}>
Click Me!
</TrackedButton>
A button-like Component can be enriched with the TrackedPressableContext HOC.
import { Button } from './components';
import { ComponentProps } from "react";
import { TrackedPressableContext } from '@objectiv/tracker-react';

<Button
onClick={handleClick}
>
Do It!
</Button>

// Tracked equivalent. The generic `ComponentProps<'button'>` is needed solely for code completion.

<TrackedPressableContext<ComponentProps<'button'>>
onClick={handleClick}
objectiv={{
Component: 'button'
}}
>
Do It!
</TrackedPressableContext>
Any element that supports onClick is compatible with the TrackedPressableContext HOC.
import { ComponentProps } from "react";
import { TrackedPressableContext } from '@objectiv/tracker-react';

<img
src="/img/ok.png"
alt="OK!"
onClick={handleClick}
/>

// Tracked equivalent. The generic `<ComponentProps<'img'>>` is needed solely for code completion.

<TrackedPressableContext<ComponentProps<'img'>>
src="/img/ok.png"
alt="OK!"
onClick={handleClick}
objectiv={{
Component: 'img'
}}
/>
reference

For a more thorough example check out the API Reference of TrackedPressableContext.

Links are interactive elements that cause a change in the current URL. Thus, we'd like to track the destination href.

// A link tag 
<a href="/somewhere">Go!</a>

// a Link component
<Link to="/cart">Back</Link>

Regular anchors can be tracked using TrackedAnchor tracked components.

import { TrackedAnchor} from '@objectiv/tracker-react';

<TrackedAnchor href="/somewhere">Go!</TrackedAnchor>

Similarly, Components that behave like Anchors can be wrapped in a LinkContext using TrackedLinkContext

import { Link, useHref } from "react-router-dom";
import { TrackedLinkContext } from '@objectiv/tracker-react';

const href = useHref(props.to);

<TrackedLinkContext<ComponentProps<typeof Link>>
{...props}
ref={ref}
objectiv={{
Component: Link,
href
}}
/>
reference

For a more thorough example check out the API Reference of TrackedLinkContext.

All Pressable may lead users to an external website and the Tracker may not have had the time to track those PressEvents.

This is why all Pressable based Components support the waitUntilTracked option. This will attempt to delay the original event handler until the Tracker has finished its job.

<TrackedAnchor to="https://www.google.com" waitUntilTracked={true}>Google</TrackedAnchor>
info

This option will not block indefinitely. It has a timeout of about 2s (Tracker Queue default batch delay * 2). That said, since the Queue is eager, under normal network conditions the wait time is barely noticeable.

In a future version of React Tracker we plan to make the waitUntilTracked option configurable inline.

Custom Components

One's own custom components may be a bit more complex than just plain Elements:

  • Provide callbacks, sometimes many, that do not necessarily match with the DOM ones.
  • Render ui fragments with multiple interactive elements each triggering its own events.

To track them we can use Location Wrappers in combination with Tracking Functions.

Let's say we want to try and track whenever a video is started or stopped via a VideoPlayer component we made.

  <VideoPlayer
src={videoUrl}
onStart={...}
onPause={...}
/>

Here is how we can:

  1. Wrap this component in the appropriate Location Context
  2. Bind tracking functions to its onStart and onPause event handlers.
    1. Tracking Functions require a Tracking Context, which is easily retrieved from the Wrapper via render-props.
import { 
MediaPlayerContextWrapper,
trackMediaPauseEvent,
trackMediaStartEvent
} from "@objectiv/tracker-react";

type TrackedVideoProps = { videoUrl: string, id: string };

const TrackedVideoPlayer = ({ videoUrl, id = "video" }: TrackedVideoProps) => (
<MediaPlayerContextWrapper id={id}>
{(trackingContext) => (
<VideoPlayer
src={videoUrl}
onStart={() => trackMediaStartEvent(trackingContext)}
onPause={() => trackMediaPauseEvent(trackingContext)}
/>
)}
</MediaPlayerContextWrapper>
);
tip

For more information, and how to avoid common pitfalls when instrumenting components with Location Wrappers and Tracking Functions, check out this Custom Components how-to guide, in which we explain the process leading to example above step-by-step.

Third Party Components

Sometimes we may want to track third party components, or components we don't have control over. A declarative approach like the one above is not always possible.

Let's try with a rather complex Component that mixes different locations and concerns: we are going to track a Layout component that can open a drawer and draw a TOC on the right.

Both the drawer and the TOC contains Links, and we have two callbacks to monitor when the drawer toggles or when a Link is clicked.

<Layout 
menuItems={[
{ type: 'sidebar', value: 'Section A', to: '/section-a'},
{ type: 'sidebar', value: 'Section B', to: '/section-b'},
{ type: 'sidebar', value: 'Section C', to: '/section-c'},
{ type: 'toc', value: 'TOC A', to: '#toc-a'},
{ type: 'toc', value: 'TOC B', to: '#toc-b'},
{ type: 'toc', value: 'TOC C', to: '#toc-c'}
]}
onDrawerToggle={(isOpen) => {
...
}}
onMenuItemClick={(menuItem) => {
...
}}
/>

Our aim is to track the two interactions with correct Locations:

Virtual Locations

We can't access the source code of the Layout component. This means we cannot just wrap the UI using Location Wrappers. While it may work for the Layout itself, it definitely would not for the Drawer, TOC and Menu Items.

Instead, we are going to create Virtual Location Contexts using Core Tracker APIs.

Also, we are going to generate our own custom callbacks, using Tracking Hooks, and preconfigure them with the Virtual Locations we made.

In the example we also show how Virtual Locations can be pushed into custom tracking callbacks, both when generating them via the hooks or later when invoking them.

import { 
makeContentContext,
makeId,
makeLinkContext,
makeNavigationContext
} from "@objectiv/tracker-core";
import {
usePressEventTracker,
useTrackVisibility
} from "@objectiv/tracker-react";

// Create some virtual location contexts
const layoutLocation = makeContentContext({id: 'layout'});
const drawerLocation = makeNavigationContext({id: 'drawer'});
const tocLocation = makeNavigationContext({id: 'toc'});

// Create an event tracker for the Drawer, preconfigured with the correct locations.
const trackDrawerVisibility = useTrackVisibility({
locationStack: [
layoutLocation,
drawerLocation
]
});

// Create another for the menu items. The item location will be added dynamically.
const trackMenuItemClick = usePressEventTracker({
locationStack: [
layoutLocation
]
});

<Layout
menuItems={[
{ type: 'sidebar', value: 'Section A', to: '/section-a' },
{ type: 'sidebar', value: 'Section B', to: '/section-b' },
{ type: 'sidebar', value: 'Section C', to: '/section-c' },
{ type: 'toc', value: 'TOC A', to: '#toc-a' },
{ type: 'toc', value: 'TOC B', to: '#toc-b' },
{ type: 'toc', value: 'TOC C', to: '#toc-c' }
]}
onDrawerToggle={() => {
trackDrawerVisibility({ isVisible: isOpen })
}}
onMenuItemClick={(menuItem) => {
trackMenuItemClick({
// Here we dynamically enrich the trackMenuItemClick Location Stack further
locationStack: [
// First we add whether this item is in the drawer or the TOC
menuItem.type === 'toc' ? tocLocation : drawerLocation,
// Then we add the item itself as LinkContext
makeLinkContext({
id: makeId(menuItem.value),
href: menuItem.to
})
]
})
}}
/>

As you can see, even without being able to use Location Wrappers, we can still generate pretty complex Location Stacks.

Next

In the next section we are going to see what Locations are used for and how to avoid Collisions.