Skip to content

Commit d8ee7a4

Browse files
authored
feat: Added feature to only track active steps (#229)
1 parent df0f3c9 commit d8ee7a4

17 files changed

+499
-34
lines changed

Diff for: README.md

+133-17
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Perfume is a tiny, web performance monitoring library that reports field data ba
2828
- 🏅 Web Vitals Score
2929
- 🛰 Flexible analytics tool
3030
- ⚡️ Waste-zero ms with [requestIdleCallback](https://developers.google.com/web/updates/2015/08/using-requestidlecallback) strategy built-in
31+
- Ability to track data about user actions
3132
<br />
3233

3334
## The latest in metrics & Real User Measurement
@@ -45,19 +46,17 @@ Perfume is a tiny, web performance monitoring library that reports field data ba
4546
- Largest Contentful Paint ([LCP](https://web.dev/lcp/))
4647
- First Input Delay ([FID](https://web.dev/fid/))
4748
- Cumulative Layout Shift ([CLS](https://web.dev/cls/))
48-
- Interaction to Next Paint ([INP](https://web.dev/inp/))
49+
- Interaction to Next Paint ([INP](https://web.dev/inp/))
4950
- Total Blocking Time ([TBT](https://web.dev/tbt/))
5051
- Navigation Total Blocking Time (NTBT)
5152

52-
5353
<br />
5454
At <a href="https://www.coinbase.com/blog/performance-vitals-a-unified-scoring-system-to-guide-performance-health-and-prioritization">Coinbase</a>, we use Perfume.js to capture a high-level scoring system that is clear, trusted, and easy to understand.
5555
<br />
5656
<br />
5757
Summarizing the performance health of an application into a reliable and consistent score helps increase urgency and directs company attention and resources towards addressing each performance opportunity.
5858
<br />
5959

60-
6160
## Perfume.js vs [Web Vitals](https://github.com/GoogleChrome/web-vitals)
6261

6362
**Perfume** leverages the [Web Vitals](https://github.com/GoogleChrome/web-vitals) library to collect all the standardized performance metrics. It explores new metrics like Navigation Total Blocking Time and dimensions like Low-End Devices, Service Worker status to understand your data better.
@@ -144,6 +143,13 @@ const perfume = new Perfume({
144143
case 'elPageTitle':
145144
myAnalyticsTool.track('elementTimingPageTitle', { duration: data });
146145
break;
146+
case 'userJourneyStep':
147+
myAnalyticsTool.track('userJourneyStep', {
148+
duration: data,
149+
stepName: attribution.step_name,
150+
vitals_score: rating
151+
});
152+
break;
147153
default:
148154
myAnalyticsTool.track(metricName, { duration: data });
149155
break;
@@ -346,21 +352,130 @@ const perfume = new Perfume({
346352
// Perfume.js: elHeroLogo 1234.00 ms
347353
```
348354

355+
### User Journey Step Tracking
356+
357+
A Step represents a slice of time in the User Journey where the user is blocked by **system time**. System time is time the system is blocking the user. For example, the time it takes to navigate between screens or fetch critical information from the server. This should not be confused with **cognitive time**, which is the time the user spends thinking about what to do next. User Journey steps should only cover system time.
358+
359+
A Step is defined by an event to start the step, and another event to end the step. These events are referred to as **Marks**.
360+
361+
As an example, a Step could be to navigate from screen A to screen B. The appropriate way to mark a start and end to this step is by marking the start when tapping on the button on screen A that starts the navigation and marking the end when screen B comes into focus with the critical data rendered on the screen.
362+
363+
```typescript
364+
// Marking the start of the step
365+
const ScreenA = () => {
366+
const handleNavigation = () => {
367+
... // Navigation logic
368+
// Mark when navigating to screen B
369+
markStep('navigate_to_screen_B');
370+
}
371+
372+
...
373+
374+
return (
375+
<>
376+
<Button onPress={handleNavigation} />
377+
</>
378+
);
379+
}
380+
381+
// Marking the end of the step
382+
const ScreenB = () => {
383+
const { viewer } = fetch("http://example.com/userInfo")
384+
.then((response) => response.json())
385+
.then((data) => data);
386+
387+
const {name} = viewer.userProperties;
388+
389+
useEffect(() => {
390+
if (name) {
391+
// Mark when data is ready for screen B
392+
markStep('loaded_screen_B');
393+
}
394+
}, [name])
395+
396+
...
397+
}
398+
```
399+
400+
#### Defining Steps
401+
In order for Perfume to be able to track metrics for Steps, we need to configure the steps and provide them when initializing Perfume.
402+
403+
Below you can find an example of how to do this.
404+
405+
``` typescript
406+
export const steps = {
407+
load_screen_A: {
408+
threshold: ThresholdTier.quick,
409+
marks: ['navigate_to_screen_A', 'loaded_screen_A'],
410+
},
411+
load_screen_B: {
412+
threshold: ThresholdTier.quick,
413+
marks: ['navigate_to_screen_B', 'loaded_screen_B'],
414+
},
415+
};
416+
417+
new Perfume ({ steps });
418+
419+
```
420+
421+
#### MarkStep
422+
`markStep` is the function used to start and end steps in applications.
423+
424+
For example, if we wanted to mark the beginning of load_screen_B step above, we would add in `markStep('navigate_to_screen_B')` to our code.
425+
426+
#### `enableNavigationTracking`
427+
428+
`enableNavigationTracking` is a boolean in the config that will tell Perfume to take into account page navigation changes or not. By default this is `true`, but it can be set to false if needed.
429+
430+
The purpose of this feature is to only account for active steps that the user is working on. The feature will remove any inactive or 'stale' steps that are not currently in progress.
431+
432+
Stale steps can be created by navigating away from a page before it fully loads, this would cause the start mark to be triggered, but the end mark to not be called. This would affect the `active` steps being returned to `onMarkStep` as well as would create incorrect data if we returned back to the end mark much later than expected.
433+
434+
`enableNavigationTracking` works together with the `incrementUjNavigation` function. The `incrementUjNavigation` function is to be called anytime there is a navigation change in your application. Below is an example for how this would work in a React Application:
435+
436+
``` typescript
437+
import { useLocation } from 'react-router-dom';
438+
439+
const MyComponent = () => {
440+
const location = useLocation()
441+
442+
React.useEffect(() => {
443+
// runs on location, i.e. route, change
444+
incrementUjNavigation();
445+
}, [location])
446+
...
447+
}
448+
449+
```
450+
349451
## Web Vitals Score
350452

351453
Perfume will expose for all major metrics the vitals score, those can be used to improve your [SEO and Google page rank](https://webmasters.googleblog.com/2020/05/evaluating-page-experience.html).
352454

353-
| Web Vitals | Good | Needs Improvement | Poor |
354-
| ----------------------------------------- | -----: | ----------------: | --------: |
355-
| Time to First Byte (TTFB) | 0-800 | 801-1800 | Over 1800 |
356-
| Redirect Time (RT) | 0-100 | 101-200 | Over 200 |
357-
| First Contentful Paint (FCP) | 0-2000 | 2001-4000 | Over 4000 |
358-
| Largest Contentful Paint (LCP) | 0-2500 | 2501-4000 | Over 4000 |
359-
| First Input Delay (FID) | 0-100 | 101-300 | Over 300 |
360-
| Cumulative Layout Shift (CLS) | 0-0.1 | 0.11-0.25 | Over 0.25 |
361-
| Interaction to Next Paint (INP) | 0-200 | 201-500 | Over 500 |
362-
| Total Blocking Time (TBT) | 0-200 | 201-600 | Over 600 |
363-
| Navigation Total Blocking Time (NTBT) | 0-200 | 201-600 | Over 600 |
455+
| Web Vitals | Good | Needs Improvement | Poor |
456+
| ------------------------------------- | -----: | ----------------: | --------: |
457+
| Time to First Byte (TTFB) | 0-800 | 801-1800 | Over 1800 |
458+
| Redirect Time (RT) | 0-100 | 101-200 | Over 200 |
459+
| First Contentful Paint (FCP) | 0-2000 | 2001-4000 | Over 4000 |
460+
| Largest Contentful Paint (LCP) | 0-2500 | 2501-4000 | Over 4000 |
461+
| First Input Delay (FID) | 0-100 | 101-300 | Over 300 |
462+
| Cumulative Layout Shift (CLS) | 0-0.1 | 0.11-0.25 | Over 0.25 |
463+
| Interaction to Next Paint (INP) | 0-200 | 201-500 | Over 500 |
464+
| Total Blocking Time (TBT) | 0-200 | 201-600 | Over 600 |
465+
| Navigation Total Blocking Time (NTBT) | 0-200 | 201-600 | Over 600 |
466+
467+
Step Tracking is based on various [thresholds](https://github.com/Zizzamia/perfume.js/blob/master/__tests__/stepsTestConstants.ts) defined.
468+
469+
Below are the thresholds available for each step:
470+
471+
| Label | Vital Thresholds |
472+
| ----------- | ---------------- |
473+
| INSTANT | [100, 200] |
474+
| QUICK | [200, 500] |
475+
| MODERATE | [500, 1000] |
476+
| SLOW | [1000, 2000] |
477+
| UNAVOIDABLE | [2000, 5000] |
478+
364479

365480
## Perfume custom options
366481

@@ -372,6 +487,7 @@ const options = {
372487
elementTiming: false,
373488
analyticsTracker: options => {},
374489
maxMeasureTime: 30000,
490+
enableNavigtionTracking: true,
375491
};
376492
```
377493

@@ -409,13 +525,13 @@ To connect with additional analytics providers, checkout the [analytics plugin f
409525
- `npm run test`: Run test suite
410526
- `npm run build`: Generate bundles and typings
411527
- `npm run lint`: Lints code
412-
<br />
528+
<br />
413529

414530
## Plugins
415531

416532
- [Perfume.js plugin for GatsbyJS](https://github.com/NoriSte/gatsby-plugin-perfume.js)
417533
- [Perfume.js plugin for Analytics](https://github.com/DavidWells/analytics/tree/master/packages/analytics-plugin-perfumejs)
418-
<br />
534+
<br />
419535

420536
## Perfume is used by
421537

@@ -427,7 +543,7 @@ To connect with additional analytics providers, checkout the [analytics plugin f
427543
- [Hearst](https://www.cosmopolitan.com/)
428544
- [Plan](https://getplan.co)
429545
- Add your company name :)
430-
<br />
546+
<br />
431547

432548
## Credits and Specs
433549

Diff for: __tests__/perfume.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { C, WN, WP } from '../src/constants';
55
import * as log from '../src/log';
66
import { metrics, ntbt } from '../src/metrics';
7-
import Perfume from '../src/perfume';
7+
import { Perfume } from '../src/perfume';
88
import * as observe from '../src/observe';
99
import { visibility } from '../src/onVisibilityChange';
1010
import { IThresholdTier } from '../src/types';

Diff for: __tests__/steps/markJourney.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44
import { WP } from '../../src/constants';
55
import mock from '../_mock';
6-
import Perfume from '../../src/perfume';
6+
import { Perfume } from '../../src/perfume';
77
import { markStep } from '../../src/steps/markStep';
88
import { steps } from '../../src/steps/steps';
99

Diff for: __tests__/steps/markJourneyOnce.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55
import { WP } from '../../src/constants';
66
import mock from '../_mock';
7-
import Perfume from '../../src/perfume';
7+
import { Perfume } from '../../src/perfume';
88
import { markStepOnce } from '../../src/steps/markStepOnce';
99

1010
import { testConfig } from '../stepsTestConstants';

Diff for: __tests__/steps/measureStep.spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44
import { WP } from '../../src/constants';
55
import mock from '../_mock';
6-
import Perfume from '../../src/perfume';
6+
import { Perfume } from '../../src/perfume';
77
import { markStep } from '../../src/steps/markStep';
88
import { config } from '../../src/config';
99

@@ -124,8 +124,8 @@ describe('measureStep', () => {
124124
);
125125
expect(analyticsTrackerSpy).toHaveBeenCalledTimes(1);
126126
expect(analyticsTrackerSpy).toHaveBeenCalledWith({
127-
attribution: { category: 'step' },
128-
metricName: 'load_second_screen_first_journey',
127+
attribution: { stepName: 'load_second_screen_first_journey' },
128+
metricName: 'userJourneyStep',
129129
rating: 'good',
130130
data: 100,
131131
navigationType: undefined,

0 commit comments

Comments
 (0)