Tracking Interactions

Now that the Tracker is up and running we can start thinking about enriching the UX with Location Wrappers and Tracked Components.

The former are generic wrapper Components while the latter can swap the original Components with an automatically wrapped and tracked version of them. Both are built on top of our Providers.

Locations?

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

Pressable and Touchable

A good rule of thumb is to start by identifying all the interactive Components in the Application and replacing them with their Tracked version.

React Navigation

If your Application uses React Navigation we highly recommend installing or at least checking the React Navigation Plugin.
This will automate tracking RootLocationContext, PathContext and commonly used navigators, such as Tabs and Drawers.

List of supported pressable Components

The React Native Tracker package comes with these ready-to-use Components.

All of them are wrapped in a PressableContext via PressableContextWrapper and have their onPress automatically tracked:

Usage example

Since these components have the same API of their non-tracked counterparts, using them is as easy as replacing them.

For example, let's look at this Button:

import { Button } from 'react-native';

<Button
onPress={onPressHandler}
title="Learn More"
/>

And its tracked version:

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

<TrackedButton
onPress={onPressHandler}
title="Learn More"
/>

The only difference between the two is that TrackedButton supports specifying a custom Context Identifier.
This is useful when:

  • The Button has a title we cannot infer a valid identifier from, e.g. an emoji
  • The Button has a title that is not satisfactory from a tracking point of view

In the following example we have a button showing an emoji with a custom identifier specified for its PressableContext

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

<TrackedButton
onPress={onPressLearnMore}
title="🏡"
id="home"
/>

And here is another example for the second scenario, where the title of the Button is not ideal for tracking analysis:

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

<TrackedButton
onPress={onPressLearnMore}
title="A very long title explaining a complex story that would not result in a nice ID"
id="home"
/>

Input Change

Inputs are another type of interaction we can track. These components never track actual values, only changes.

List of supported input Components

The React Native Tracker package comes with these ready-to-use Components.

All of them are wrapped in a InputContext via InputContextWrapper and have their events automatically tracked:

Usage example

These components have the same API of their non-tracked counterparts, using them is as easy as replacing them.

Let's look at an example with a TextInput:

import { TextInput } from "react-native";

const CustomTextInput = () => {
const [text, onChangeText] = React.useState("Some Text");

return <TextInput onChangeText={onChangeText} value={text} />;
};

The tracked version is identical, we just swap TextInput with TrackedTextInput:

import { TrackedTextInput } from "@objectiv/tracker-react-native";

const CustomTextInput = () => {
const [text, onChangeText] = React.useState("Some Text");

return <TrackedTextInput id="custom-input" onChangeText={onChangeText} value={text} />;
};

Custom and Third Party Components

More complex components can be more difficult to track than the native ones. They may:

  • Provide multiple callbacks for different concerns.
  • Render multiple interactive elements each triggering its own events.

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

Custom components

Let's say we want to try and track a custom components that allow interacting with Images.
This component has all the props of regular Images, plus an onPress event to perform an action.

Our component may look something like this:

type PressableImageProps = ImageProps & {
onPress?: null | ((event: GestureResponderEvent) => void) | undefined;
};

const PressableImage = ({ onPress, ...imageProps }: PressableImageProps) => (
<Pressable
onPress={onPress}
>
<Image {...imageProps} />
</Pressable>
);

Here is how we can:

  1. Wrap this component in the appropriate Location Context
  2. Bind a tracking function to its onPress handler.
    1. Tracking Functions require a Tracking Context, which is easily retrieved from the Wrapper via render-props.
import { 
PressableContextWrapper,
trackPressEvent
} from "@objectiv/tracker-react-native";

type TrackedPressableImageProps = PressableImageProps & {
id: string,
};

const TrackedPressableImage = ({ id, onPress, ...imageProps }: TrackedPressableImageProps) => (
<PressableContextWrapper id={id}>
{(trackingContext) => (
<PressableImage
onPress={(event) => {
onPress && onPress(event);
trackPressEvent(trackingContext);
}}
{...imageProps}
/>
)}
</PressableContextWrapper>
);
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 also draw tabs on the bottom.

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

<Layout 
links={[
{ type: 'drawer', value: 'Section A', to: '/section-a'},
{ type: 'drawer', value: 'Section B', to: '/section-b'},
{ type: 'drawer', value: 'Section C', to: '/section-c'},
{ type: 'tabs', value: 'Tab A', to: '/tab-a'},
{ type: 'tabs', value: 'Tab B', to: '/tab-b'},
{ type: 'tabs', value: 'Tab C', to: '/tab-c'}
]}
onDrawerToggle={(isOpen) => {
...
}}
onLinkPress={(link) => {
...
}}
/>

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, Tabs and Links.

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-native";

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

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

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

<Layout
links={[
{ type: 'drawer', value: 'Section A', to: '/section-a'},
{ type: 'drawer', value: 'Section B', to: '/section-b'},
{ type: 'drawer', value: 'Section C', to: '/section-c'},
{ type: 'tabs', value: 'Tab A', to: '/tab-a'},
{ type: 'tabs', value: 'Tab B', to: '/tab-b'},
{ type: 'tabs', value: 'Tab C', to: '/tab-c'}
]}
onDrawerToggle={() => {
trackDrawerVisibility({ isVisible: isOpen })
}}
onLinkPress={(link) => {
trackLinkPress({
// Here we dynamically enrich the trackLinkPress Location Stack further
locationStack: [
// First we add whether this item is in the drawer or the tabs
link.type === 'tabs' ? tabsLocation : drawerLocation,
// Then we add the item itself as LinkContext
makeLinkContext({
id: makeId(link.value),
href: link.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.