{"id":25969604,"url":"https://github.com/dropbox/ttvc","last_synced_at":"2025-03-04T22:48:26.691Z","repository":{"id":37793314,"uuid":"467682411","full_name":"dropbox/ttvc","owner":"dropbox","description":"Measure Visually Complete metrics in real time","archived":false,"fork":false,"pushed_at":"2024-09-13T20:09:25.000Z","size":712,"stargazers_count":42,"open_issues_count":2,"forks_count":8,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-02-28T23:38:48.097Z","etag":null,"topics":["performance","user-centric-metrics","web-performance","web-vitals"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@dropbox/ttvc","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dropbox.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":"AUTHORS","dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-03-08T21:32:47.000Z","updated_at":"2025-01-03T03:52:19.000Z","dependencies_parsed_at":"2024-04-01T17:46:20.785Z","dependency_job_id":"f660bdab-9749-4e8d-8ac2-3e592b29693c","html_url":"https://github.com/dropbox/ttvc","commit_stats":{"total_commits":103,"total_committers":11,"mean_commits":9.363636363636363,"dds":"0.44660194174757284","last_synced_commit":"18d5eb126d61991e69094df425f70d37cfb27463"},"previous_names":[],"tags_count":28,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dropbox%2Fttvc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dropbox%2Fttvc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dropbox%2Fttvc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dropbox%2Fttvc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dropbox","download_url":"https://codeload.github.com/dropbox/ttvc/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241935237,"owners_count":20044826,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["performance","user-centric-metrics","web-performance","web-vitals"],"created_at":"2025-03-04T22:48:24.511Z","updated_at":"2025-03-04T22:48:26.684Z","avatar_url":"https://github.com/dropbox.png","language":"TypeScript","funding_links":[],"categories":["Tools"],"sub_categories":["RUM"],"readme":"# `ttvc`\n\n![version](https://img.shields.io/npm/v/@dropbox/ttvc)\n![minzip size](https://img.shields.io/bundlephobia/minzip/@dropbox/ttvc)\n\n![lint](https://img.shields.io/github/actions/workflow/status/dropbox/ttvc/lint.yml?branch=main\u0026label=lint)\n![unit](https://img.shields.io/github/actions/workflow/status/dropbox/ttvc/unit.yml?branch=main\u0026label=unit)\n![playwright](https://img.shields.io/github/actions/workflow/status/dropbox/ttvc/playwright.yml?branch=main\u0026label=playwright)\n\n- [Overview](#overview)\n- [Get started](#get-started)\n- [Usage](#usage)\n  - [Basic usage](#basic-usage)\n  - [Report metrics to a collection endpoint](#report-metrics-to-a-collection-endpoint)\n  - [Record a PerformanceTimeline entry](#record-a-performancetimeline-entry)\n  - [Client-side navigation with React Router](#client-side-navigation-with-react-router)\n  - [Attributing TTVC measurement cancellations](#attributing-ttvc-measurement-cancellations)\n- [API](#api)\n  - [Types](#types)\n  - [Functions](#functions)\n- [Browser Support](#browser-support)\n- [How does it work?](#how-does-it-work)\n- [Developing](#developing)\n  - [Building](#building)\n  - [Testing](#testing)\n  - [Releasing](#releasing)\n\n## Overview\n\n`ttvc` provides an in-browser implementation of the VisuallyComplete metric suitable for field data collection (real-user monitoring).\n\nVisually Complete measures the moment in time when users perceive that all the visual elements of a page have completely loaded. Once the page has reached visually complete, nothing else should change in the viewport without the user’s input.\n\n## Get started\n\nThis library is available from npm. Add it to your project using the `npm` or `yarn` package managers.\n\n```\n$ npm install @dropbox/ttvc\n```\n\n```\n$ yarn add @dropbox/ttvc\n```\n\n## Usage\n\n### Basic usage\n\n```js\nimport {init, onTTVC} from '@dropbox/ttvc';\n\n// Call this as early in pageload as possible to setup instrumentation.\ninit({\n  debug: false,\n  idleTimeout: 2000,\n  networkTimeout: 0,\n});\n\n// Reports the last visible change for each navigation that\n// occurs during the life of this document.\nconst unsubscribe = onTTVC((measurement) =\u003e {\n  console.log('TTVC:', measurement.duration);\n});\n```\n\n### Report metrics to a collection endpoint\n\n```js\nimport {init, onTTVC} from '@dropbox/ttvc';\n\ninit();\n\nlet measurements = [];\n\n// capture measurements in client\nonTTVC((measurement) =\u003e {\n  measurements.append(measurement);\n});\n\n// flush data to server when page is hidden\ndocument.addEventListener('visibilitychange', () =\u003e {\n  if (document.visibilityState === 'hidden') {\n    navigator.sendBeacon('/log', JSON.stringify(measurements));\n    measurements = [];\n  }\n});\n```\n\n### Record a PerformanceTimeline entry\n\nCapture a span using the [Performance Timeline](https://developer.mozilla.org/en-US/docs/Web/API/Performance_Timeline) API.\n\nNOTE: Setting arbitrary start and end times with `performance.measure` relies on the [User Timing Level 3](https://w3c.github.io/user-timing/) specification. This is not yet adopted by all major browsers.\n\n```js\nimport {init, onTTVC} from '@dropbox/ttvc';\n\ninit();\n\nonTTVC(({start, end, duration, detail}: Metric) =\u003e {\n  window.performance.measure('TTVC', {\n    start,\n    end,\n    duration,\n    detail,\n  });\n});\n```\n\n### Client-side navigation with React Router\n\n@dropbox/ttvc supports measuring client-side navigations!\n\nWhat counts as navigation may be different in each application, but as long as you signal that a navigation has begun, this library can figure out the rest.\n\nTo trigger a new navigation measurement, call `start()` or dispatch a \"locationchange\" event on the window object.\n\n```js\n// analytics.js\nimport {init, onTTVC} from '@dropbox/ttvc';\n\ninit();\n\nonTTVC((measurement) =\u003e {\n  console.log('TTVC:', measurement.duration);\n});\n```\n\n```js\n// app.js\nimport {start} from '@dropbox/ttvc';\nimport React, {useEffect} from 'react';\nimport ReactDOM from 'react-dom';\nimport {BrowserRouter, useLocation} from 'react-router-dom';\n\nReactDOM.render(\n  \u003cBrowserRouter\u003e\n    \u003cApp /\u003e\n  \u003c/BrowserRouter\u003e,\n  document.getElementById('root')\n);\n\nconst App = () =\u003e {\n  const location = useLocation();\n\n  useEffect(() =\u003e {\n    // Option 1: If you have access to the ttvc library, import it and\n    // call start().\n    start();\n\n    // Option 2: Dispatch a custom 'locationchange' event. TTVC subscribes to\n    // this and will call start() for you.\n    window.dispatchEvent(new Event('locationchange'));\n  }, [location]);\n\n  return (\n    \u003cdiv className=\"App\"\u003e\n      \u003ch1\u003eWelcome to React Router!\u003c/h1\u003e\n      \u003cRoutes\u003e\n        \u003cRoute path=\"/\" element={\u003cHome /\u003e} /\u003e\n        {/* ... more routes */}\n      \u003c/Routes\u003e\n    \u003c/div\u003e\n  );\n};\n```\n\n### Attributing TTVC measurement cancellations\n\nIn certain cases, @dropbox/ttvc might discard the measurement before it is captured.\n\nThis can happen if a user interacts with or navigates away from the page, or the page is put in the background before it has reached a state ot visual completeness.\n\nThis is done to obtain a higher confidence of the measurement's accuracy, as interaction with a page can cause it to change in ways that invalidate the measurement.\n\nHowever, @dropbox/ttvc provides a way to monitor these cancellations and attribute them to a specific cause. A second callback function provided to `onTTVC` will be called when the measurement is cancelled.\n\n```js\nimport {init, onTTVC} from '@dropbox/ttvc';\n\ninit();\n\nonTTVC(\n  (measurement) =\u003e {\n    console.log('TTVC measurement captured:', measurement.duration);\n  },\n  (error) =\u003e {\n    console.log('TTVC measurement cancelled:', error.cancellationReason);\n  }\n);\n```\n\n## API\n\n### Types\n\n#### `Metric`\n\n```typescript\nexport type Metric = {\n  // time since timeOrigin that the navigation was triggered\n  // (this will be 0 for the initial pageload)\n  start: number;\n\n  // time since timeOrigin that ttvc was marked for the current navigation\n  end: number;\n\n  // the difference between start and end; this is the value of \"TTVC\"\n  duration: number;\n\n  // additional metadata related to the current navigation\n  detail: {\n    // if ttvc ignored a stalled network request, this value will be true\n    didNetworkTimeOut: boolean;\n\n    // the most recent visible update\n    // (this can be either a mutation or a load event target, whichever\n    // occurred last)\n    lastVisibleChange?: HTMLElement | TimestampedMutationRecord;\n\n    // describes how the navigation being measured was initiated\n    // NOTE: this extends the navigation type values defined in the W3 spec;\n    // \"script\" is usually reported as \"navigation\" by the browser, but we\n    // report that distinctly\n    // @see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/type\n    navigationType: // Navigation started by clicking a link, by entering the\n    // URL in the browser's address bar, or by form submission.\n    | 'navigate'\n      // Navigation is through the browser's reload operation.\n      | 'reload'\n      // Navigation is through the browser's history traversal operation.\n      | 'back_forward'\n      // Navigation is initiated by a prerender hint.\n      | 'prerender'\n      // Navigation is triggered with a script operation, e.g. in a single page application.\n      | 'script';\n  };\n};\n```\n\n#### `CancellationError`\n\n```typescript\nexport type CancellationError = {\n  // time since timeOrigin that the navigation was triggered\n  start: number;\n\n  // time since timeOrigin that cancellation occurred\n  end: number;\n\n  // the difference between start and end\n  duration: number;\n\n  // reason for cancellation\n  cancellationReason: CancellationReason;\n\n  // Optional type of event that triggered cancellation\n  eventType?: string;\n\n  // Optional target of event that triggered cancellation\n  eventTarget?: EventTarget;\n\n  // the most recent visual update; this can be either a mutation or a load event target\n  lastVisibleChange?: HTMLElement | TimestampedMutationRecord;\n\n  navigationType: NavigationType;\n};\n\nexport enum CancellationReason {\n  // navigation has occurred\n  NEW_NAVIGATION = 'NEW_NAVIGATION',\n\n  // page was put in background\n  VISIBILITY_CHANGE = 'VISIBILITY_CHANGE',\n\n  // user interaction occurred\n  USER_INTERACTION = 'USER_INTERACTION',\n\n  // measurement was cancelled because a new one was started\n  NEW_MEASUREMENT = 'NEW_MEASUREMENT',\n\n  // manual cancellation via API happened\n  MANUAL_CANCELLATION = 'MANUAL_CANCELLATION',\n}\n```\n\n#### `TtvcOptions`\n\n```typescript\nexport type TtvcOptions = {\n  // decide whether to log debug messages\n  debug?: boolean;\n\n  // the duration in ms to wait before declaring the page completely idle\n  idleTimeout?: number;\n\n  // a duration in ms to wait before assuming that a single network request\n  // was not instrumented correctly\n  networkTimeout?: number;\n};\n```\n\n### Functions\n\n#### `init()`\n\n```typescript\ntype init = (options?: TtvcOptions) -\u003e void;\n```\n\nSets up instrumentation for the current page and begins monitoring. For the most accurate results, call this as early in pageload as possible.\n\nAccepts an optional options argument (see above).\n\n#### `start()`\n\n```typescript\ntype start = () -\u003e void;\n```\n\nStart a new TTVC measurement.\n\nYou _do not_ need to call this for the initial page load. Use this to notify `@dropbox/ttvc` that a new client-side navigation is about to take place.\n\n#### `onTTVC()`\n\n```typescript\ntype onTTVC = (subscriber: (metric: Metric) -\u003e void) -\u003e () =\u003e void;\n```\n\nRegister a callback function as a subscriber to new TTVC metric measurements. Returns an \"unsubscribe\" function which may be called to unregister the subscribed callback function.\n\nThe callback function may be called more than once if in-page navigation occurs.\n\n`onTTVC` may be called more than once to register more than one subscriber.\n\n#### `cancel()`\n\n```typescript\ntype cancel = (eventType?: string) =\u003e void;\n```\n\nAbort the current TTVC measurement.\n\nThis method is provided as an escape hatch. Consider using `cancel` to notify @dropbox/ttvc that a user interaction has occurred and continuing the measurement may produce an invalid result.\n\nAn optional argument can be passed specifying the event that triggered the cancellation. Type of that event will be logged along with the cancellation to the error callback.\n\n#### `incrementAjaxCount() \u0026 decrementAjaxCount()`\n\n```typescript\ntype incrementAjaxCount = () -\u003e void;\ntype decrementAjaxCount = () -\u003e void;\n```\n\nUse these functions to instrument AJAX requests in your application. Try to ensure `incrementAjaxCount` and `decrementAjaxCount` are called exactly once for each request, regardless of success or failure.\n\ne.g.\n\n```javascript\n// patch window.fetch\nconst nativeFetch = window.fetch;\n\nwindow.fetch = (...args) =\u003e {\n  TTVC.incrementAjaxCount();\n  return nativeFetch(...args).finally(TTVC.decrementAjaxCount);\n};\n```\n\n## Browser Support\n\n`@dropbox/ttvc` relies on the following browser features.\n\n- [ES6 syntax](https://262.ecma-international.org/6.0/)\n- [async/await syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)\n- [window.MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)\n- [window.IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)\n- [performance.now()](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now)\n\nSee browsers that support all above features on caniuse.com:\n\nhttps://caniuse.com/mutationobserver,async-functions,mdn-api_intersectionobserver,es6,high-resolution-time\n\nIf you would like to use this library and support a browser that does not support one of these features, you will be responsible for supplying polyfills and/or recompiling the library to remove unsupported syntax.\n\n## How does it work?\n\nBrowsers today don't automatically report much about their paint cycles. It would definitely be an improvement if browser developers decided to report this value for us.\n\nIn the meantime, we approximate TTVC using a heuristic based on a combination of observing mutations and \"load\" events.\n\n![The TTVC Algorithm](https://user-images.githubusercontent.com/11449340/217963862-617c379c-2efd-4cc3-9c74-aa58afe7b1da.png)\n\nFor a more detailed write-up of the architecture, check out our announcement blog post!\n\nhttps://dropbox.tech/frontend/measuring-ttvc-web-performance-metric-open-source-library\n\n## Developing\n\nTo develop this package, you will need node version 16 or greater, and the `yarn` package manager. Once you have these prerequisites, install project dependencies with:\n\n```\nyarn install\n```\n\n### Building\n\nThis project is developed with TypeScript. You can compile the TypeScript source files to JavaScript with:\n\n```\n$ yarn build\n```\n\nWhile testing locally, you may find it useful to build the rollup bundle in watch mode.\n\n```\n$ yarn build:rollup --watch\n```\n\n### Testing\n\nYou can run all tests together with:\n\n```\n$ yarn test\n```\n\n#### Individual tests\n\nThere are four individual test scripts\n\n```\n$ yarn test:lint // runs eslint\n$ yarn test:typecheck // runs typescript with --noEmit\n$ yarn test:unit // runs jest unit tests\n$ yarn test:e2e // runs playwright tests (requires yarn build to have been run)\n```\n\n#### Testing with Playwright\n\nBefore running any playwright tests, you will need to install the default set of browsers:\n\n```\n$ npx playwright install\n```\n\nRun test suite:\n\n```\n$ yarn build // build the ttvc package before testing\n$ yarn test:e2e\n```\n\nTo manually test pages, start the test server.\n\n```\n$ yarn express\n```\n\nYou can launch tests in the browser by opening http://localhost:3000/test/[test-folder]. e.g. http://localhost:3000/test/ajax1/\n\n### Releasing\n\n\u003e NOTE: This package should be released by a repository administrator.  New versions are expected to be pushed directly to `main`.\n\nThis package is configured to publish new releases using [`np`](https://github.com/sindresorhus/np#readme).\n\nTo cut a new release, switch to the `main` branch and run `npm run release`.\n\n\u003e NOTE: Despite the fact that this package is managed with yarn, there is currently a bug in np preventing `yarn np` from running correctly.\n\nThis should automate the process of releasing by doing the following:\n\n- prompt you to select a new version\n- install all dependencies\n- run tests\n- build a new compiled artifact\n- bump the version in package.json\n- push the new version to npm\n- create a new tag and associated \"release\" on github.com\n\n`np` supports a number of cli flags which may also be useful for you:\n\n- `npm run np -- --preview` Do a dry run to test that your release will behave as expected\n- `npm run np -- patch|minor|major` bump the version to the next patch/minor/major increment\n- `npm run np -- prerelease --tag=beta` create an \"prerelease\" version which will _not_ automatically be marked as `latest`\n\nSee the [full documentation](https://github.com/sindresorhus/np#readme) for all `np` CLI options.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdropbox%2Fttvc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdropbox%2Fttvc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdropbox%2Fttvc/lists"}