# @financial-times/o-tracking

> Provides tracking for a product. Tracking requests are sent to the Spoor API.

Latest version **4.12.0** (published 2026-06-25) · MIT license · 0 weekly downloads

## Install

```sh
npm install @financial-times/o-tracking
pnpm add @financial-times/o-tracking
yarn add @financial-times/o-tracking
bun add @financial-times/o-tracking
```

## Health

**Score 60/100 (C)** — status: active.

Positive: esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types.

## Facts

| | |
|---|---|
| Version | 4.12.0 |
| Published | 2026-06-25 |
| First published | 2019-02-15 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | none |
| Module format | ESM |
| Dependencies | 1 |
| Unpacked size | 158 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | the-ft, rowanmanning, chee, alexwilson, aendra, emmalewis, notlee, hamza.samih, robertboulton, leila.micollier, robgodfrey |
| Keywords | tracking, spoor |

## Links

- npm: https://www.npmjs.com/package/@financial-times/o-tracking
- Homepage: https://registry.origami.ft.com/components/o-tracking
- Issues: https://github.com/Financial-Times/origami/issues/new?labels=o-tracking,libraries
- npm.io page: https://npm.io/package/@financial-times/o-tracking

## Dependencies (1)

- [ftdomdelegate](https://npm.io/package/ftdomdelegate.md) ^5

## Alternatives

- [@sentry/react-native](https://npm.io/package/@sentry/react-native.md) — 2.6M weekly downloads
- [@ardatan/aggregate-error](https://npm.io/package/@ardatan/aggregate-error.md) — 708.1K weekly downloads
- [custom-error-generator](https://npm.io/package/custom-error-generator.md) — 2.0K weekly downloads
- [@technik-sde/prosemirror-recreate-transform](https://npm.io/package/@technik-sde/prosemirror-recreate-transform.md) — 1.5K weekly downloads
- [@suchipi/error-utils](https://npm.io/package/@suchipi/error-utils.md) — 78 weekly downloads

## Recent versions

- 4.12.0 (latest) — 2026-06-25
- 3.2.0 (tag--v3.2.0) — 2022-03-25
- 3.1.4 (tag--v3.1.4) — 2021-07-07
- 3.1.3 (tag--v3.1.3) — 2021-06-24
- 4.0.0-beta.0 (prerelease) — 2021-04-29
- 4.11.0 — 2026-06-24
- 5.0.0 — 2026-06-22
- 4.10.0 — 2025-09-10
- 4.9.0 — 2025-06-17
- 4.8.0 — 2025-06-02
- 4.7.0 — 2025-01-30
- 4.6.1 — 2024-11-18
- 4.6.0 — 2024-11-14
- 4.5.4 — 2024-05-01
- 4.5.3 — 2024-04-30
- … 45 more at https://npm.io/package/@financial-times/o-tracking/versions

## README

# o-tracking

Include in your product to send tracking requests to the [Spoor API](https://spoor-docs.herokuapp.com/).

- [Usage](#usage)
  - [Tracking without JavaScript](#tracking-without-javascript)
  - [Tracking with JavaScript](#tracking-with-javascript)
  - [How to track extra click data using `data-trackable` attributes](#how-to-track-extra-click-data-using-data-trackable-attributes)
- [How to view the data](#how-to-view-the-data)
- [Example implementations](#example-implementations)
- [Migration Guide](#migration-guide)
- [Contact](#contact)
- [Licence](#licence)


## Usage

Check out [how to include Origami components in your project](../../docs/legacy-o2-docs/technical-guide.md) to get started with `o-tracking`.

### Tracking without JavaScript

o-tracking does not work without JavaScript but, you can still send basic tracking requests to the Spoor API by setting a css background image url which points to the Spoor tracking pixel endpoint (`https://runtime-api.ft.com/px.gif`). Click events and other interactive events will not be tracked without JavaScript but basic page view events can be tracked.

The Spoor tracking pixel endpoint takes a `data` query parameter which is a url-encoded JSON string that represents the data to track in Spoor.

```html
<div class="o-tracking o--if-no-js" data-o-component="o-tracking">
	<div style="background: url('https://runtime-api.ft.com/px.gif?data=YOUR_URL_ENCODED_JSON_DATA_HERE');"></div>
</div>
```


For example, if you have the following data you want to track in Spoor:

_Recommendation:_ Using a different `system.source` value from the one used in o-tracking JavaScript mode will help your project be able to more easily group the data for those users without JavaScript. The example below uses the source `o-tracking-fallback`.

```json
{
  "category": "page",
  "action": "view",
  "system": {
    "source": "o-tracking-fallback",
  },
  "context": {
    "product": "ft.com",
    "content": {
      "asset_type": "page"
    }
  }
}
```
Here is the corresponding tracking pixel setup:
```html
<div class="o-tracking o--if-no-js" data-o-component="o-tracking">
    <div style="background: url('https://runtime-api.ft.com/px.gif?data=%7B%22category%22:%22page%22,%20%22action%22:%22view%22,%20%22system%22:%7B%22apiKey%22:%22qUb9maKfKbtpRsdp0p2J7uWxRPGJEP%22,%22source%22:%22o-tracking%22,%22version%22:%221.0.0%22%7D,%22context%22:%7B%22product%22:%22ft.com%22,%22content%22:%7B%22asset_type%22:%22page%22%7D%7D%7D');"></div>
</div>
```

### Tracking with JavaScript

### Turning on offline support and queueing events

By default o-tracking uses the browser Beacon API to send events to Spoor and the Beacon API only works when the device is online and o-tracking can not queue those events.

If wanting to queue events (to retry them if they failed to send) and/or wanting to record events when offline, set `queue` to `true` during the intialisation of `o-tracking` like so:

```js
import oTracking from '@financial-times/o-tracking';

const config = {
    queue: true, // Make sure o-tracking stores the events on a local queue (this means o-tracking can work when the device is offline)
    ...
};
oTracking.init(config);
```

#### Developer/Test Mode

o-tracking has a development/test mode which will mark the events as test events, and also write to the console extra debugging information.

To activate the mode, set `test` to `true` during the intialisation of `o-tracking` like so:

```js
import oTracking from '@financial-times/o-tracking';

const config = {
    test: true, // Mark the events as test events and turn on extra debug logging.
    ...
};
oTracking.init(config);
```

o-tracking also has a `test-data` mode to mark events as test events without the debugging information in the console:

```js
import oTracking from '@financial-times/o-tracking';

const config = {
    test_data: true, // Mark the events as test events without extra debug logging.
    ...
};
oTracking.init(config);
```

#### Methods

##### oTracking.init

To manually instantiate `o-tracking`, import the component and call the `init` method with configuration which is specific to your product:

``` js
import oTracking from '@financial-times/o-tracking';

const config = {
    context: {
        // This value is used as a way to identify the high-level product, for example: ft.com, FT app, biz-ops etc.
        product: 'o-tracking-example',
    }
};
oTracking.init(config);
```

##### oTracking.page

To track a page view for the product you call the `oTracking.page` method.
Page events automatically track the url and the referrer. Refer to the [page event document](docs/event-parameters/page-view.md) for information about properties which can be set.
```js
const pageConfig = {
  content: {
      /*
        Asset type is meant to describe the main purpose of the page
        The value can be one of these:
        - `story` - A story or article
        - `blog` - A blog post
        - `front` - A home page or front page
        - `ad` - An advert.
        - `image` - An image
        - `interactive` - An interactive graphic
        - `report` - A special report
        - `search` - A search results page
        - `section` - A section or listing page
        - `topic` -  A topic landing page
        - `video` - A video page
        - `login` - Any login/sign-in page
        - `stream` - A stream page
        - `funnel` - Any funnel page
        - `epaper` - all epaper pages
        - `rankings` - A rankings page for schools and courses (i.e. on rankings.ft.com). Not the section's hub page
        - `markets` - Any market, bond, commoditity, stock, currency 'tearsheet' (usually has "/tearsheet/" in URL)
        - `myft` - All MyFT pages
        - `account` - All account pages
        - `membership` - All membership pages
        - `page` - anything else, not above.
    */
    asset_type: "story"
  }
};
oTracking.page(pageConfig);
```

##### oTracking.click.init

Call the `oTracking.click.init` method to track click events. Pass the category you would like the click events to have as an argument:

- o-tracking click events will also track the path from the root of the DOM tree to the element which was clicked. This is recorded in a property called `domPathTokens`.
- If the clicked element has the `data-trackable` attribute set, sibling elements will also be included within the `domPathTokens` property.

Please refer to the [event document](docs/event.md) for information about all the possible properties which can be set.
```js
// Tracking a click
const category = 'element';
oTracking.click.init(category);
```

_Note:_ The attribute `data-o-tracking-skip-queue` is no longer used, it is now the default behaviour.*

##### oTracking.view.init

To track when an element has come into view of the user, add the attribute `data-o-tracking-view` to the element in the page and then call the `oTracking.view.init` method:

- By default, elements with the `data-o-tracking-view` attribute are tracked.
- To track different elements, set the `selector` option property to a CSS selector.
- Like click events, view events will also track the path from the root of the DOM tree to the element which triggered the tracking event into a property called `domPathTokens`.
- To categorise the view events, set the `category` option property.
- To customise the how much of this element needs to be in the viewport to trigger the event, use the `intersectionObserverThreshold` option. This value will be set as the threshold for the IntersectionObserver (it defaults to `1.0`).
- To collect extra data to send with the tracking event, add a function named `getContextData` to the options. The function receives as it's single argument the element which triggered the tracking event and needs to return an object with any of these optional properties set:
	- `componentContentId`
	- `type`
	- `subtype`
	- `component`

UPDATE 2025-060-12: THESE OPTIONS MAY BE OUT OF DATE, THEY DEFINITELY ARE FOR COMPONENTS, SEE BELOW FOR UPDATED INFORMATION

_Note:_ This feature requires `window.IntersectionObserver` in order to track the events
_Note:_ `getContextData` should be a function which returns `{object}`. It accepts the viewed element as an argument

###### Component tracking
[Component standard template tracking spec](https://docs.google.com/spreadsheets/d/16d2-uDM8rhz1wSKOkoupcpPDRFQ8AtY1GxVJpt74vY4/edit?gid=1318706009#gid=1318706009) sets out what data should be captured for component views. Some are generated for you, `category` and `action` have defaults. You will need to set the following properties in `getContextData`

 ```js
 getContextData: () => {
      return {
        component: {
          name: '[some-human-readable-name]',
          type: '[see tracking spec list]',
          id: '[consistent-guid]'
        },
        content: {
          asset_type: '[see tracking spec list]',
          uuid: ['content-id-eg-article-id']
        }
      };
    },
  });
```

Optionally you can also add in up to 3 custom values for extra metadata withing the getContextData call
```js
    ...
        custom: [
          {
            name: 'some-name',
            value: 'some-value',
          },
        ],
    ...
```


Please refer to the [event document](docs/event.md) for information about all the possible properties which can be set.

```js
const opts = {
    category: 'audio', // default: 'component'
    selector: '.o-teaser__audio', // default: '[data-o-tracking-view]'
    getContextData: (el) => {  // default: null
        return {
            componentContentId: el.getAttribute('data-id'),
            type: 'audio',
            subtype: 'podcast',
            component: 'teaser'
        };
    }
};

oTracking.view.init(opts);
```

##### oTracking.event

To track a custom event call the `oTracking.event` method. For example to track that a video is played:
```js
const eventConfig = {
    "category": "video", // optional
    "action": "play", // optional
};
oTracking.event(eventConfig);
```

The `event` method also accepts an optional callback which is run after the event has been processed.
```js
const eventConfig = {
    "category": "video", // optional
    "action": "play", // optional
};
oTracking.event(eventConfig, () => {
  console.log('The custom video event has been processed!');
});
```

Please refer to the [event document](docs/event.md) for information about all the possible properties which can be set.


#### Events

Instead of calling the `page` and `event` methods `o-tracking` can be configured to capture events based on the custom DOM Events `oTracking.page` and `oTracking.event`.

##### oTracking.page

Call the `oTracking.page.init()` method to listen for `oTracking.page` events:
```js
// Tell o-tracking to listen for `oTracking.page` events
oTracking.page.init();

// Now we can send page events and o-tracking will track the data within them.
const pageData = { content: { uuid: 'abc-123', barrier: 'PREMIUM' };
document.body.dispatchEvent(new CustomEvent('oTracking.page', { detail: pageData}, bubbles: true}));
```

##### oTracking.event

To make o-tracking listen for `oTracking.event` events, you call `oTracking.event.init()`:
```js
// Tell o-tracking to listen for `oTracking.event` events
oTracking.event.init();

// Now we can send custom events and o-tracking will track the data within them.
const customData = { category: 'video', action: 'play', id: '512346789', pos: '10' };
document.body.dispatchEvent(new CustomEvent('oTracking.event', { detail: customData}, bubbles: true}));
```


### How to track extra click data using `data-trackable` attributes

You can add extra information to o-tracking events by using the `data-trackable` and `data-trackable-context-*` custom attributes.

#### `data-trackable`

`o-tracking` will walk up the DOM to get the tree of elements with `data-trackable` attributes. For example, clicks to the list-item button below will result in an event that has this path recorded within it - `header-menu country-selector china`:-

```html
<div data-trackable="header-menu">
	<span>
		<div data-trackable="country-selector">
			<ul>
				<li data-trackable="china"><button>China</button></li>
			</ul>
		</div>
	</span>
</div>
```

#### `data-trackable-context-*`

To add extra context to events you may add custom attributes in the form: `data-trackable-context-name="data"` where `name` is the name of the extra context and `data` is the extra data.

For example, when the below anchor element is clicked, it will add extra event fields `article_guid` and `article_source` to the data being tracked.

```html
<a href="https://www.ft.com/content/1234-1234-1234-1234"
	data-trackable="view-original-article"
	data-trackable-context-article_guid="1234-1234-1234-1234"
	data-trackable-context-article_source="FINANCIAL TIMES">
```

**Nested properties**

To add a nested property to the context of events using the `data-trackable-context-*` custom attribute, specify the string representation of the value in a JSON-like format.

For example, when the below anchor element is clicked:

```html
<a href="https://www.ft.com/content/1234-1234-1234-1234"
	data-trackable="view-original-article"
  data-trackable-context-plain_property="plain-property-value"
	data-trackable-context-nested_property='{ "type": "nested", "value": "nested-property-test" }'>
```
The event context will be provided with the properties: `plain_property` and `nested_property`, as shown in the fragment below:

```json
{
  "category": "link",
  "action": "click",
  "system": {
    "source": "o-tracking",
  },
  "context": {
    "plain_property": "plain-property-value",
    "nested_property": {
      "type": "nested",
      "value": "nested-property-test"
    }
  }
}
```

## How to view the data

Once you have sent data into Spoor, you can view the data via Looker.

The FT has a Slack channel named #ask-an-analyst where anyone can get help with Looker.

If you have never used Looker before, you will need to [request access via this form](https://fteunew.freshservice.com/support/catalog/items/73).

To sign in to Looker:
- Open the [FT Looker](https://ft.okta.com/home/looker/0oacqdp1byXmj5Zcz357/63357) in your browser.


## Example implementations

- [ft.com](docs/ftcom_example.md)
- [membership](docs/membership_example.md)


## Migration Guide

| State | Major Version | Last Minor Release | Migration guide |
| :---: | :---: | :---: | :---: |
| ✨ active | 4 | N/A | [migrate to v4](MIGRATION.md#migrating-from-v3-to-v4) |
| ⚠ maintained | 3 | 3.1| [migrate to v3](MIGRATION.md#migrating-from-v2-to-v3) |
| ╳ deprecated | 2 | 2.0.10 | [migrate to v2](MIGRATION.md#migrating-from-v1-to-v2) |
| ╳ deprecated | 1 | 1.7.2 | N/A |

***

## Contact

If you have any questions or comments about this component, or need help using it, please either [raise an issue](https://github.com/Financial-Times/o-tracking/issues), visit [#origami-support](https://financialtimes.slack.com/messages/origami-support/) or email [Origami Support](mailto:origami-support@ft.com).

***

## Licence

This software is published by the Financial Times under the [MIT licence](http://opensource.org/licenses/MIT).

---
_Source: https://npm.io/package/@financial-times/o-tracking · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
