New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

remix-use-spa-metrics

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

remix-use-spa-metrics - npm Package Compare versions

Comparing version 0.1.0 to 0.2.0

18

dist/index.d.ts

@@ -19,4 +19,20 @@ export declare enum RemixPerfMark {

state: "idle" | "submitting" | "loading";
location: {
pathname: string;
search: string;
};
}
export declare function useSpaMetrics(navigation: Navigation): void;
export interface CallbackData {
type: "submitting" | "loading";
fromLocation: Location;
toLocation: Location;
finalLocation: Location;
submissionDuration?: number;
loadingDuration: number;
totalDuration: number;
}
export interface CallbackFunction {
(data: CallbackData): void;
}
export declare function useSpaMetrics(location: Location, navigation: Navigation, callback: CallbackFunction): void;
export {};

124

dist/remix-use-spa-metrics.es.js

@@ -0,1 +1,20 @@

var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
import * as React from "react";

@@ -28,5 +47,8 @@ var RemixPerfMark = /* @__PURE__ */ ((RemixPerfMark2) => {

}
function getLastMark() {
let marks = window.performance.getEntriesByType("mark");
return findRight(marks, (m) => m.detail.id === navId);
function getLastNavEntry(type, predicate) {
let marks = window.performance.getEntriesByType(type);
return findRight(marks, (m) => {
var _a;
return ((_a = m.detail) == null ? void 0 : _a.id) === navId && (!predicate || predicate(m));
});
}

@@ -37,52 +59,76 @@ function wasSubmissionNavigation() {

}
function useSpaMetrics(navigation) {
function mark(name, detail) {
return window.performance.mark(name, {
detail: __spreadProps(__spreadValues({}, detail), {
id: navId
})
});
}
function measure(name, start, end) {
return window.performance.measure(name, {
start,
end,
detail: {
id: navId
}
});
}
function completeNavigation(type, callback) {
let startMark = getLastNavEntry("mark", (m) => m.detail.fromLocation != null);
let submissionMeasure = type === "submitting" ? getLastNavEntry("measure", (m) => m.name === "\u{1F4BF}:submitting") : null;
let loadingMeasure = getLastNavEntry("measure", (m) => m.name === "\u{1F4BF}:loading");
let navMeasure = getLastNavEntry("measure", (m) => m.name === (type === "submitting" ? "\u{1F4BF}:submission-navigation" : "\u{1F4BF}:loading-navigation"));
if (!startMark || !navMeasure || !loadingMeasure) {
console.error("Could not find proper nav marks/measures, skipping SPA metrics");
return;
}
callback({
type,
fromLocation: startMark.detail.fromLocation,
toLocation: startMark.detail.toLocation,
finalLocation: location,
submissionDuration: submissionMeasure == null ? void 0 : submissionMeasure.duration,
loadingDuration: loadingMeasure.duration,
totalDuration: navMeasure.duration
});
}
function useSpaMetrics(location2, navigation, callback) {
React.useEffect(() => {
if (navigation.state === "idle") {
if (!getLastMark()) {
if (!getLastNavEntry("mark")) {
return;
}
if (wasSubmissionNavigation()) {
window.performance.mark("\u{1F4BF}:loading-end", {
detail: { id: navId }
});
window.performance.mark("\u{1F4BF}:submission-navigation-end", {
detail: { id: navId }
});
window.performance.measure("\u{1F4BF}:submission-navigation", "\u{1F4BF}:submission-navigation-start", "\u{1F4BF}:submission-navigation-end");
window.performance.measure("\u{1F4BF}:loading", "\u{1F4BF}:loading-start", "\u{1F4BF}:loading-end");
mark("\u{1F4BF}:loading-end");
mark("\u{1F4BF}:submission-navigation-end");
measure("\u{1F4BF}:submission-navigation", "\u{1F4BF}:submission-navigation-start", "\u{1F4BF}:submission-navigation-end");
measure("\u{1F4BF}:loading", "\u{1F4BF}:loading-start", "\u{1F4BF}:loading-end");
completeNavigation("submitting", callback);
} else {
window.performance.mark("\u{1F4BF}:loading-end", {
detail: { id: navId }
});
window.performance.mark("\u{1F4BF}:loading-navigation-end", {
detail: { id: navId }
});
window.performance.measure("\u{1F4BF}:loading-navigation", "\u{1F4BF}:loading-navigation-start", "\u{1F4BF}:loading-navigation-end");
window.performance.measure("\u{1F4BF}:loading", "\u{1F4BF}:loading-start", "\u{1F4BF}:loading-end");
mark("\u{1F4BF}:loading-end");
mark("\u{1F4BF}:loading-navigation-end");
measure("\u{1F4BF}:loading-navigation", "\u{1F4BF}:loading-navigation-start", "\u{1F4BF}:loading-navigation-end");
measure("\u{1F4BF}:loading", "\u{1F4BF}:loading-start", "\u{1F4BF}:loading-end");
completeNavigation("loading", callback);
}
navId++;
} else if (navigation.state === "submitting") {
navId++;
window.performance.mark("\u{1F4BF}:submission-navigation-start", {
detail: { id: navId }
mark("\u{1F4BF}:submission-navigation-start", {
fromLocation: location2,
toLocation: navigation.location
});
window.performance.mark("\u{1F4BF}:submitting-start", {
mark("\u{1F4BF}:submitting-start", {
detail: { id: navId }
});
} else if (navigation.state === "loading") {
if (!getLastMark()) {
navId++;
window.performance.mark("\u{1F4BF}:loading-navigation-start", {
detail: { id: navId }
if (!getLastNavEntry("mark")) {
mark("\u{1F4BF}:loading-navigation-start", {
fromLocation: location2,
toLocation: navigation.location
});
window.performance.mark("\u{1F4BF}:loading-start", {
detail: { id: navId }
});
mark("\u{1F4BF}:loading-start");
} else {
window.performance.mark("\u{1F4BF}:submitting-end", {
detail: { id: navId }
});
window.performance.measure("\u{1F4BF}:submitting", "\u{1F4BF}:submitting-start", "\u{1F4BF}:submitting-end");
window.performance.mark("\u{1F4BF}:loading-start", {
detail: { id: navId }
});
mark("\u{1F4BF}:submitting-end");
measure("\u{1F4BF}:submitting", "\u{1F4BF}:submitting-start", "\u{1F4BF}:submitting-end");
mark("\u{1F4BF}:loading-start");
}

@@ -89,0 +135,0 @@ }

@@ -1,1 +0,1 @@

(function(a,o){typeof exports=="object"&&typeof module!="undefined"?o(exports,require("react")):typeof define=="function"&&define.amd?define(["exports","react"],o):(a=typeof globalThis!="undefined"?globalThis:a||self,o(a.useSpaMetrics={},a.React))})(this,function(a,o){"use strict";function g(i){if(i&&i.__esModule)return i;var t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});return i&&Object.keys(i).forEach(function(e){if(e!=="default"){var F=Object.getOwnPropertyDescriptor(i,e);Object.defineProperty(t,e,F.get?F:{enumerable:!0,get:function(){return i[e]}})}}),t.default=i,Object.freeze(t)}var m=g(o),u=(i=>(i.SubmissionNavigationStart="\u{1F4BF}:submission-navigation-start",i.SubmissionNavigationEnd="\u{1F4BF}:submission-navigation-end",i.LoadingNavigationStart="\u{1F4BF}:loading-navigation-start",i.LoadingNavigationEnd="\u{1F4BF}:loading-navigation-end",i.SubmittingStart="\u{1F4BF}:submitting-start",i.SubmittingEnd="\u{1F4BF}:submitting-end",i.LoadingStart="\u{1F4BF}:loading-start",i.LoadingEnd="\u{1F4BF}:loading-end",i))(u||{}),d=(i=>(i.SubmissionNavigation="\u{1F4BF}:submission-navigation",i.Submitting="\u{1F4BF}:submitting",i.Loading="\u{1F4BF}:loading",i.LoadingNavigation="\u{1F4BF}:loading-navigation",i))(d||{});let n=0;function r(i,t){for(let e=i.length-1;e>=0;e--)if(t(i[e]))return i[e]}function s(){let i=window.performance.getEntriesByType("mark");return r(i,t=>t.detail.id===n)}function l(){let i=window.performance.getEntriesByType("mark");return r(i,t=>t.detail.id===n&&t.name==="\u{1F4BF}:submission-navigation-start")}function f(i){m.useEffect(()=>{if(i.state==="idle"){if(!s())return;l()?(window.performance.mark("\u{1F4BF}:loading-end",{detail:{id:n}}),window.performance.mark("\u{1F4BF}:submission-navigation-end",{detail:{id:n}}),window.performance.measure("\u{1F4BF}:submission-navigation","\u{1F4BF}:submission-navigation-start","\u{1F4BF}:submission-navigation-end"),window.performance.measure("\u{1F4BF}:loading","\u{1F4BF}:loading-start","\u{1F4BF}:loading-end")):(window.performance.mark("\u{1F4BF}:loading-end",{detail:{id:n}}),window.performance.mark("\u{1F4BF}:loading-navigation-end",{detail:{id:n}}),window.performance.measure("\u{1F4BF}:loading-navigation","\u{1F4BF}:loading-navigation-start","\u{1F4BF}:loading-navigation-end"),window.performance.measure("\u{1F4BF}:loading","\u{1F4BF}:loading-start","\u{1F4BF}:loading-end"))}else i.state==="submitting"?(n++,window.performance.mark("\u{1F4BF}:submission-navigation-start",{detail:{id:n}}),window.performance.mark("\u{1F4BF}:submitting-start",{detail:{id:n}})):i.state==="loading"&&(s()?(window.performance.mark("\u{1F4BF}:submitting-end",{detail:{id:n}}),window.performance.measure("\u{1F4BF}:submitting","\u{1F4BF}:submitting-start","\u{1F4BF}:submitting-end"),window.performance.mark("\u{1F4BF}:loading-start",{detail:{id:n}})):(n++,window.performance.mark("\u{1F4BF}:loading-navigation-start",{detail:{id:n}}),window.performance.mark("\u{1F4BF}:loading-start",{detail:{id:n}})))},[i.state])}a.RemixPerfMark=u,a.RemixPerfMeasure=d,a.useSpaMetrics=f,Object.defineProperties(a,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
(function(a,o){typeof exports=="object"&&typeof module!="undefined"?o(exports,require("react")):typeof define=="function"&&define.amd?define(["exports","react"],o):(a=typeof globalThis!="undefined"?globalThis:a||self,o(a.useSpaMetrics={},a.React))})(this,function(a,o){"use strict";var E=Object.defineProperty,j=Object.defineProperties;var O=Object.getOwnPropertyDescriptors;var v=Object.getOwnPropertySymbols;var _=Object.prototype.hasOwnProperty,h=Object.prototype.propertyIsEnumerable;var p=(a,o,e)=>o in a?E(a,o,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[o]=e,S=(a,o)=>{for(var e in o||(o={}))_.call(o,e)&&p(a,e,o[e]);if(v)for(var e of v(o))h.call(o,e)&&p(a,e,o[e]);return a},L=(a,o)=>j(a,O(o));function e(i){if(i&&i.__esModule)return i;var n=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});return i&&Object.keys(i).forEach(function(t){if(t!=="default"){var s=Object.getOwnPropertyDescriptor(i,t);Object.defineProperty(n,t,s.get?s:{enumerable:!0,get:function(){return i[t]}})}}),n.default=i,Object.freeze(n)}var w=e(o),m=(i=>(i.SubmissionNavigationStart="\u{1F4BF}:submission-navigation-start",i.SubmissionNavigationEnd="\u{1F4BF}:submission-navigation-end",i.LoadingNavigationStart="\u{1F4BF}:loading-navigation-start",i.LoadingNavigationEnd="\u{1F4BF}:loading-navigation-end",i.SubmittingStart="\u{1F4BF}:submitting-start",i.SubmittingEnd="\u{1F4BF}:submitting-end",i.LoadingStart="\u{1F4BF}:loading-start",i.LoadingEnd="\u{1F4BF}:loading-end",i))(m||{}),c=(i=>(i.SubmissionNavigation="\u{1F4BF}:submission-navigation",i.Submitting="\u{1F4BF}:submitting",i.Loading="\u{1F4BF}:loading",i.LoadingNavigation="\u{1F4BF}:loading-navigation",i))(c||{});let d=0;function f(i,n){for(let t=i.length-1;t>=0;t--)if(n(i[t]))return i[t]}function F(i,n){let t=window.performance.getEntriesByType(i);return f(t,s=>{var l;return((l=s.detail)==null?void 0:l.id)===d&&(!n||n(s))})}function y(){let i=window.performance.getEntriesByType("mark");return f(i,n=>n.detail.id===d&&n.name==="\u{1F4BF}:submission-navigation-start")}function u(i,n){return window.performance.mark(i,{detail:L(S({},n),{id:d})})}function g(i,n,t){return window.performance.measure(i,{start:n,end:t,detail:{id:d}})}function B(i,n){let t=F("mark",r=>r.detail.fromLocation!=null),s=i==="submitting"?F("measure",r=>r.name==="\u{1F4BF}:submitting"):null,l=F("measure",r=>r.name==="\u{1F4BF}:loading"),b=F("measure",r=>r.name===(i==="submitting"?"\u{1F4BF}:submission-navigation":"\u{1F4BF}:loading-navigation"));if(!t||!b||!l){console.error("Could not find proper nav marks/measures, skipping SPA metrics");return}n({type:i,fromLocation:t.detail.fromLocation,toLocation:t.detail.toLocation,finalLocation:location,submissionDuration:s==null?void 0:s.duration,loadingDuration:l.duration,totalDuration:b.duration})}function N(i,n,t){w.useEffect(()=>{if(n.state==="idle"){if(!F("mark"))return;y()?(u("\u{1F4BF}:loading-end"),u("\u{1F4BF}:submission-navigation-end"),g("\u{1F4BF}:submission-navigation","\u{1F4BF}:submission-navigation-start","\u{1F4BF}:submission-navigation-end"),g("\u{1F4BF}:loading","\u{1F4BF}:loading-start","\u{1F4BF}:loading-end"),B("submitting",t)):(u("\u{1F4BF}:loading-end"),u("\u{1F4BF}:loading-navigation-end"),g("\u{1F4BF}:loading-navigation","\u{1F4BF}:loading-navigation-start","\u{1F4BF}:loading-navigation-end"),g("\u{1F4BF}:loading","\u{1F4BF}:loading-start","\u{1F4BF}:loading-end"),B("loading",t)),d++}else n.state==="submitting"?(u("\u{1F4BF}:submission-navigation-start",{fromLocation:i,toLocation:n.location}),u("\u{1F4BF}:submitting-start",{detail:{id:d}})):n.state==="loading"&&(F("mark")?(u("\u{1F4BF}:submitting-end"),g("\u{1F4BF}:submitting","\u{1F4BF}:submitting-start","\u{1F4BF}:submitting-end"),u("\u{1F4BF}:loading-start")):(u("\u{1F4BF}:loading-navigation-start",{fromLocation:i,toLocation:n.location}),u("\u{1F4BF}:loading-start")))},[n.state])}a.RemixPerfMark=m,a.RemixPerfMeasure=c,a.useSpaMetrics=N,Object.defineProperties(a,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
{
"name": "remix-use-spa-metrics",
"version": "0.1.0",
"version": "0.2.0",
"description": "Custom hook for tracking client-side navigation performance in Remix/React-Router applications",

@@ -39,9 +39,14 @@ "author": "matt@brophy.org",

"build": "vite build && tsc -b",
"dev": "vite dev"
"dev": "vite dev",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"react": ">=16.8"
"react": ">=16.8",
"react-dom": ">=16.8"
},
"devDependencies": {
"@types/node": "17.0.40",
"@types/react": "18.0.10",
"@vitejs/plugin-react": "1.3.2",
"react-router-dom": "6.4.0-pre.2",
"typescript": "4.7.2",

@@ -48,0 +53,0 @@ "vite": "2.9.9"

# remix-use-spa-metrics
Custom hook for tracking client-side navigation performance in Remix/React-Router applications
## Motivation
The [current performance landscape](https://web.dev/vitals/#core-web-vitals) focuses primarily on SSR metrics, but largely ignores any standardized set of metrics for subsequent SPA navigations. This is understandable since the approach used for SPA navigations varies so much from app to tpp that it would likely be incredibly hard to come up with a on-size-fits-all set of metrics.
However, if you're building a universal app that SSR's and then hydrates into a SPA - that SSR page load may only account for a small percentage of your users page load sin their session. shouldn't we be looking at the stats for pages 2 through N?
This custom hook aims to facilitate your own standardized set of measurements for your Remix/React-Router (>=6.4.0) by leveraging the [`useNavigation`](https://beta.reactrouter.com/en/v6.4.0-pre.2/hooks/use-navigation) (`useTransition` in Remix) hook which tells you when your application is performing a SPA navigation between pages.
You can then send the data off to whatever performance tracking setup you use. In a former life, I did this through [Blue Tringle Custom Timers](https://help.bluetriangle.com/hc/en-us/articles/360039526094-Custom-Timer-Implementation).
## Installation
```bash
npm install remix-use-spa-metrics
# or
yarn add remix-use-spa-metrics
```
## Usage
The best way to leverage this is to put a single root layout wrapper around your entire application, and then wire up the hook a single time in the root:
```jsx
import { useSpaMetrics } from "remix-use-spa-metrics";
function RootLayout() {
let location = useLocation();
let navigation = useNavigation();
let callback = React.useCallback((data) => {
// This function is called once at the end of each navigation
}, []);
useSpaMetrics(location, navigation, callback);
...
}
```
Then, you can use the callback function to send the data off to your
performance tracking service. The `data` parameter is of the following shape:
```typescript
interface CallbackData {
type: "submitting" | "loading";
fromLocation: Location;
toLocation: Location;
finalLocation: Location;
submissionDuration?: number;
loadingDuration: number;
totalDuration: number;
}
```
On standard `<Link>` loading navigations, you'll get an approximately equal `loadingDuration` and `totalDuration`. Non-GET `<Form>` submissions will provide a `submissionDuration` and a `loadingDuration` that will sum up to `totalDuration`
`fromLocation` and `toLocation` provide the `useLocation()` and `useNavigation().location` values from the start of the navigation. `finalLocation` reflects the final `useLocation()` whn we returned to an `idle` state since we may have redirected a few times while in the `loading` state.
Eventually, this hook may provide more granular detail about encountered redirects and subsequent load times but for now we view "navigations" from the user's eyes - as a single duration from the moment they click a Link or submit a Form until they are viewing the resulting page.
### `window.performance.mark` and `window.performance.measure`
Under the hood, this is all implemented using the [`window.performance.mark`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark) and [`window.performance.measure`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure) APIs, so you automatically get the built-in dev tools functionality there, such as showing the measurements in the Profiler and access to your individual marks via `window.performance.getEntries*`:
![Chrome Dev Tools Profiler View](./devtools.png)
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc