Options
All
  • Public
  • Public/Protected
  • All
Menu

NOTE: The only difference between this interface and the normal Tracker is how the start method works. Creating nested timings introduces new edge cases that are important for you to understand:

Edge Cases

Calling stop() Multiple Times

Normally, invoking the stop() function returned from start() multiple times will create a separate timing entry for each invocation and increase the entry's count property.

With a nested timer, that only holds true for the root timing. For nested timings, calling stop() multiple times creates sibling entries, incrementing count with each invocation:

import { tracker } from '~/tracker';

const logger = trackers.utils.withNesting(tracker);

async function makeParallelDataCalls(start) {
const [stop] = start('parallel calls');
await Promise.all([
someDataCall().then(() => stop()),
someOtherDataCall().then(() => stop()),
someLastDataCall().then(() => stop())
]);
}

export async function loadData() {
const [stop, start] = logger.start('load data');
await makeParallelDataCalls(start);
stop();
}

timing tree:

{
"id": "9c6f8a25-5003-4b17-b3d6-838144c54a7d",
"label": "load data",
"start": 1562933463457,
"stop": 1562933463490,
"duration": 33,
"type": "timer",
"count": 1,
"data": {
"children": [
{
"label": "parallel calls",
"count": 1,
"start": 1562933463458,
"stop": 1562933463488,
"duration": 30,
"data": {
"children": []
}
},
{
"label": "parallel calls",
"count": 2,
"start": 1562933463458,
"stop": 1562933463490,
"duration": 32,
"data": {
"children": []
}
},
{
"label": "parallel calls",
"count": 3,
"start": 1562933463458,
"stop": 1562933463490,
"duration": 32,
"data": {
"children": []
}
}
]
}
}

Stopping Parents Before Children

It is okay for nested timings to stop after an ancestor timing stops. However, when the root timing is stopped, only completed timings will appear in the timing tree. In other words, any nested timings that are still running will not appear in the timing tree.

import { tracker } from '~/tracker';

const logger = trackers.utils.withNesting(tracker);

async function childData(start) {
const [stop] = start('child timing');
await someDataCall();
stop();
}

export async function loadData() {
const [stop, start] = logger.start('load data');
childData(start); // BUG! we forgot to await this async function!
// because we didn't wait for childData to complete, the next line
// will invoke stop WHILE the async function is still running...
stop();
}

timing tree:

{
"id": "ca0f72ad-eb9a-4b07-96ec-6292b8d2317f",
"label": "load data",
"start": 1562936590429,
"stop": 1562936590440,
"duration": 11,
"type": "timer",
"count": 1,
"data": {
"children": []
}
}

Creating Child Trackers

Even on a NestedTimingTracker, calling child() creates a normal Tracker instance. So, if you call start() on a child Tracker, it will not use nested timings. If you want to combine child Trackers with nested timings, you should change your call order:

import { tracker } from '~/tracker';

// INCORRECT ✘
const logger = trackers.utils.withNesting(tracker);
const child = logger.child();

// CORRECT ✓
const child = tracker.child();
const logger = trackers.utils.withNesting(child);

Best Practices

If you need to create a nested timing, that is a good indication that the code should exist in a separate function. When you call this function, you should pass the nested start function so that function can continue the pattern by creating any nested timings it needs (now or in the future):

import { tracker } from '~/tracker';

const logger = trackers.utils.withNesting(tracker);

// INCORRECT ✘
export async function loadData() {
const [stop, start] = logger.start('load data');
start('nested timing');
await someDataCall();
stop();
}

// CORRECT ✓
export async function loadData() {
const [stop, start] = logger.start('load data');
await loadChildData(start);
stop();
}

async function loadChildData(start) {
const [stop, nest] = start('nested timing');
// now we can pass `nest` to another function to
// continue our pattern of creating nested timings
await someDataCall();
stop();
}

Hierarchy

Index

Methods

  • Creates a child Tracker instance.

    example
    import { tracker } from '~/tracking';

    // this tracker will inherit any context data
    // set in landing's tracker while also mixing
    // in any contextual data of its own
    export const myAppTracker = tracker.child();

    myAppTracker.context({ app: 'my-app' });
    myAppTracker.event('app tracker created');

    Returns Tracker

    A new Tracker instance that will notify the same root subscriber of TrackingInfo entries, mixing in ancestor contextual data as needed.

  • context(data: Record<string, any>): void
  • Sets contextual data to be mixed into each TrackingInfo created by this Tracker or any child Trackers.

    example
    import { get } from 'lodash';
    import { store, tracker } from '~/tracking';

    store.subscribe(() => {
    const state = store.getState();
    const app = get(state, 'routes.stage');
    const drawer = get(state, 'routes.drawer');
    tracker.context({ app, drawer });
    });

    Parameters

    • data: Record<string, any>

      The data to merge into any TrackingInfo instances created by this (or child) Tracker methods.

    Returns void

  • error(err: Error): void
  • Logs an Error.

    example
    import { tracker } from '~/tracking';

    export function doSomething(param) {
    somePromiseMethod()
    .catch(errors.rethrow({ param }))
    .catch(tracker.error);
    }

    Parameters

    • err: Error

      The Error instance to log.

    Returns void

  • event(label: string, data?: Record<string, any>): void
  • Logs an event. Events usually represent important points in an application's lifecycle or user-initiated actions such as button clicks.

    NOTE: This method also creates a browser performance mark with the given message name.

    example
    import { tracker } from '~/tracking';

    window.addEventListener('click', (e) => {
    if (e.target.matches('button, a')) {
    // could grab additional contextual data
    // by looking at ancestor elements' attributes
    const type = e.target.tagName.toLowerCase();
    tracker.event('click', {
    tags: ['ui', type],
    label: e.target.innerText
    });
    }
    });

    Parameters

    • label: string

      The name of the event to log.

    • Optional data: Record<string, any>

      Optional information to associate with this TrackingInfo.

    Returns void

  • Starts a timing tree. Unlike the normal start method, this method does not return a stop function. Instead, it returns an array. The first value in the array is the stop function; the second argument is another start function you can invoke to begin a new nested timing.

    example
    import { tracker } from '~/tracking';
    import { someDataCall, someOtherDataCall } from '~/data/operations';

    const child = tracker.child();
    const logger = trackers.utils.withNesting(child);

    export async function loadData(id) {
    try {
    const [stop, start] = logger.start('load data');
    const data = await someDataCall(id);
    const results = await loadNestedData(start, data);
    stop({ id, results });
    return results;
    } catch (e) {
    logger.error(e);
    }
    }

    async function loadNestedData(start, data) {
    const [stop, ] = start('load nested data');
    const results = await someOtherDataCall(data);
    stop();
    return results;
    }

    Parameters

    • label: string

      The label of the nested timer to create.

    Returns NestedStartResult

    The [stop, start] methods you can use to end the current timing or start a nested timing. The first function is a normal TimerStopFunction and the second function is another NestedTimingTracker.start function.

  • uuid(): string
  • Generates a random RFC 4122 UUID guaranteed to be unique.

    example
    import { tracker } from '~/tracking';
    import { proxy } from '~/path/to/data';

    proxy.use({
    headers: {
    'x-session-id': tracker.uuid()
    },
    match: {
    base: '^my\-app' // can use regular expression syntax
    }
    });

    Returns string