@codeserk/forge-stats-web-vitals 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 codeserk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,19 @@
1
- # Temporary Holding Version
1
+ # @codeserk/forge-stats-web-vitals
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Web vitals plugin for [`@codeserk/forge-stats`](../stats/README.md). It re-exports the whole core client, so a web app imports this package alone. The core is a peer dependency, installed with it by npm 7+, so the app always has a single client.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @codeserk/forge-stats-web-vitals
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Captures LCP, INP, CLS, FCP and TTFB with `web-vitals`, plus the DOMContentLoaded (`domLoad`) and load (`pageLoad`) times, and tracks them as one `Performance` event per page load when the page is first hidden. `getRoute` returns the route pattern, not the raw pathname. A metric that did not happen is left out:
14
+
15
+ ```ts
16
+ import { init, webVitals } from '@codeserk/forge-stats-web-vitals'
17
+
18
+ init({ sdk: 'YOUR_SDK_KEY', plugins: [webVitals({ getRoute: () => router.currentRoute.pattern })] })
19
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,72 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let web_vitals = require("web-vitals");
3
+ //#region src/web-vitals.const.ts
4
+ const PERFORMANCE_EVENT_TYPE = "Performance";
5
+ const WEB_VITALS_PLUGIN_NAME = "web-vitals";
6
+ //#endregion
7
+ //#region src/web-vitals.ts
8
+ /**
9
+ * Returns the plugin that tracks the web vitals of the page load as one `Performance` event.
10
+ * @param options
11
+ * @returns the plugin for `init`
12
+ */
13
+ function webVitals(options) {
14
+ return {
15
+ name: WEB_VITALS_PLUGIN_NAME,
16
+ setup: (client) => captureWebVitals(client, options)
17
+ };
18
+ }
19
+ /**
20
+ * Tracks the web vitals through the client the first time the page is hidden, when INP and CLS are final.
21
+ * @param client
22
+ * @param options
23
+ */
24
+ function captureWebVitals(client, { getRoute }) {
25
+ const metrics = {};
26
+ const collect = (metric) => {
27
+ metrics[metric.name.toLowerCase()] = metric.value;
28
+ };
29
+ (0, web_vitals.onLCP)(collect);
30
+ (0, web_vitals.onINP)(collect);
31
+ (0, web_vitals.onCLS)(collect);
32
+ (0, web_vitals.onFCP)(collect);
33
+ (0, web_vitals.onTTFB)(collect);
34
+ const onVisibilityChange = () => {
35
+ if (document.visibilityState !== "hidden") return;
36
+ document.removeEventListener("visibilitychange", onVisibilityChange);
37
+ client.track({
38
+ type: PERFORMANCE_EVENT_TYPE,
39
+ name: getRoute(),
40
+ data: {
41
+ context: "web",
42
+ ...metrics,
43
+ ...loadTimes()
44
+ }
45
+ });
46
+ };
47
+ document.addEventListener("visibilitychange", onVisibilityChange);
48
+ }
49
+ /**
50
+ * Returns the DOMContentLoaded and load times reached so far.
51
+ * @returns the times in ms, as `domLoad` and `pageLoad`
52
+ */
53
+ function loadTimes() {
54
+ const [navigation] = performance.getEntriesByType("navigation");
55
+ const times = {};
56
+ if (navigation?.domContentLoadedEventEnd > 0) times.domLoad = navigation.domContentLoadedEventEnd;
57
+ if (navigation?.loadEventEnd > 0) times.pageLoad = navigation.loadEventEnd;
58
+ return times;
59
+ }
60
+ //#endregion
61
+ exports.webVitals = webVitals;
62
+ var _codeserk_forge_stats = require("@codeserk/forge-stats");
63
+ Object.keys(_codeserk_forge_stats).forEach(function(k) {
64
+ if (k !== "default" && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
65
+ enumerable: true,
66
+ get: function() {
67
+ return _codeserk_forge_stats[k];
68
+ }
69
+ });
70
+ });
71
+
72
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/web-vitals.const.ts","../src/web-vitals.ts"],"sourcesContent":["export const PERFORMANCE_EVENT_TYPE = 'Performance'\nexport const WEB_CONTEXT = 'web'\nexport const WEB_VITALS_PLUGIN_NAME = 'web-vitals'\n","import type { Client, StatsPlugin } from '@codeserk/forge-stats'\nimport { type Metric, onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals'\n\nimport { PERFORMANCE_EVENT_TYPE, WEB_CONTEXT, WEB_VITALS_PLUGIN_NAME } from './web-vitals.const'\nimport type { WebVitalsOptions } from './web-vitals.types'\n\n/**\n * Returns the plugin that tracks the web vitals of the page load as one `Performance` event.\n * @param options\n * @returns the plugin for `init`\n */\nexport function webVitals(options: WebVitalsOptions): StatsPlugin {\n return {\n name: WEB_VITALS_PLUGIN_NAME,\n setup: (client) => captureWebVitals(client, options),\n }\n}\n\n/**\n * Tracks the web vitals through the client the first time the page is hidden, when INP and CLS are final.\n * @param client\n * @param options\n */\nfunction captureWebVitals(client: Client, { getRoute }: WebVitalsOptions): void {\n const metrics: Record<string, number> = {}\n const collect = (metric: Metric) => {\n metrics[metric.name.toLowerCase()] = metric.value\n }\n onLCP(collect)\n onINP(collect)\n onCLS(collect)\n onFCP(collect)\n onTTFB(collect)\n\n const onVisibilityChange = () => {\n if (document.visibilityState !== 'hidden') {\n return\n }\n\n document.removeEventListener('visibilitychange', onVisibilityChange)\n client.track({\n type: PERFORMANCE_EVENT_TYPE,\n name: getRoute(),\n data: { context: WEB_CONTEXT, ...metrics, ...loadTimes() },\n })\n }\n document.addEventListener('visibilitychange', onVisibilityChange)\n}\n\n/**\n * Returns the DOMContentLoaded and load times reached so far.\n * @returns the times in ms, as `domLoad` and `pageLoad`\n */\nfunction loadTimes(): Record<string, number> {\n const [navigation] = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[]\n const times: Record<string, number> = {}\n if (navigation?.domContentLoadedEventEnd > 0) {\n times.domLoad = navigation.domContentLoadedEventEnd\n }\n if (navigation?.loadEventEnd > 0) {\n times.pageLoad = navigation.loadEventEnd\n }\n\n return times\n}\n"],"mappings":";;;AAAA,MAAa,yBAAyB;AAEtC,MAAa,yBAAyB;;;;;;;;ACStC,SAAgB,UAAU,SAAwC;AAChE,QAAO;EACL,MAAM;EACN,QAAQ,WAAW,iBAAiB,QAAQ,QAAQ;EACrD;;;;;;;AAQH,SAAS,iBAAiB,QAAgB,EAAE,YAAoC;CAC9E,MAAM,UAAkC,EAAE;CAC1C,MAAM,WAAW,WAAmB;AAClC,UAAQ,OAAO,KAAK,aAAa,IAAI,OAAO;;AAE9C,EAAA,GAAA,WAAA,OAAM,QAAQ;AACd,EAAA,GAAA,WAAA,OAAM,QAAQ;AACd,EAAA,GAAA,WAAA,OAAM,QAAQ;AACd,EAAA,GAAA,WAAA,OAAM,QAAQ;AACd,EAAA,GAAA,WAAA,QAAO,QAAQ;CAEf,MAAM,2BAA2B;AAC/B,MAAI,SAAS,oBAAoB,SAC/B;AAGF,WAAS,oBAAoB,oBAAoB,mBAAmB;AACpE,SAAO,MAAM;GACX,MAAM;GACN,MAAM,UAAU;GAChB,MAAM;IAAE,SAAA;IAAsB,GAAG;IAAS,GAAG,WAAW;IAAE;GAC3D,CAAC;;AAEJ,UAAS,iBAAiB,oBAAoB,mBAAmB;;;;;;AAOnE,SAAS,YAAoC;CAC3C,MAAM,CAAC,cAAc,YAAY,iBAAiB,aAAa;CAC/D,MAAM,QAAgC,EAAE;AACxC,KAAI,YAAY,2BAA2B,EACzC,OAAM,UAAU,WAAW;AAE7B,KAAI,YAAY,eAAe,EAC7B,OAAM,WAAW,WAAW;AAG9B,QAAO"}
@@ -0,0 +1,19 @@
1
+ import { StatsPlugin } from "@codeserk/forge-stats";
2
+ export * from "@codeserk/forge-stats";
3
+
4
+ //#region src/web-vitals.types.d.ts
5
+ interface WebVitalsOptions {
6
+ /** Returns the route pattern, not the raw pathname. @example '/products/:id' */
7
+ readonly getRoute: () => string;
8
+ }
9
+ //#endregion
10
+ //#region src/web-vitals.d.ts
11
+ /**
12
+ * Returns the plugin that tracks the web vitals of the page load as one `Performance` event.
13
+ * @param options
14
+ * @returns the plugin for `init`
15
+ */
16
+ declare function webVitals(options: WebVitalsOptions): StatsPlugin;
17
+ //#endregion
18
+ export { type WebVitalsOptions, webVitals };
19
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,19 @@
1
+ import { StatsPlugin } from "@codeserk/forge-stats";
2
+ export * from "@codeserk/forge-stats";
3
+
4
+ //#region src/web-vitals.types.d.ts
5
+ interface WebVitalsOptions {
6
+ /** Returns the route pattern, not the raw pathname. @example '/products/:id' */
7
+ readonly getRoute: () => string;
8
+ }
9
+ //#endregion
10
+ //#region src/web-vitals.d.ts
11
+ /**
12
+ * Returns the plugin that tracks the web vitals of the page load as one `Performance` event.
13
+ * @param options
14
+ * @returns the plugin for `init`
15
+ */
16
+ declare function webVitals(options: WebVitalsOptions): StatsPlugin;
17
+ //#endregion
18
+ export { type WebVitalsOptions, webVitals };
19
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,63 @@
1
+ import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
2
+ export * from "@codeserk/forge-stats";
3
+ //#region src/web-vitals.const.ts
4
+ const PERFORMANCE_EVENT_TYPE = "Performance";
5
+ const WEB_VITALS_PLUGIN_NAME = "web-vitals";
6
+ //#endregion
7
+ //#region src/web-vitals.ts
8
+ /**
9
+ * Returns the plugin that tracks the web vitals of the page load as one `Performance` event.
10
+ * @param options
11
+ * @returns the plugin for `init`
12
+ */
13
+ function webVitals(options) {
14
+ return {
15
+ name: WEB_VITALS_PLUGIN_NAME,
16
+ setup: (client) => captureWebVitals(client, options)
17
+ };
18
+ }
19
+ /**
20
+ * Tracks the web vitals through the client the first time the page is hidden, when INP and CLS are final.
21
+ * @param client
22
+ * @param options
23
+ */
24
+ function captureWebVitals(client, { getRoute }) {
25
+ const metrics = {};
26
+ const collect = (metric) => {
27
+ metrics[metric.name.toLowerCase()] = metric.value;
28
+ };
29
+ onLCP(collect);
30
+ onINP(collect);
31
+ onCLS(collect);
32
+ onFCP(collect);
33
+ onTTFB(collect);
34
+ const onVisibilityChange = () => {
35
+ if (document.visibilityState !== "hidden") return;
36
+ document.removeEventListener("visibilitychange", onVisibilityChange);
37
+ client.track({
38
+ type: PERFORMANCE_EVENT_TYPE,
39
+ name: getRoute(),
40
+ data: {
41
+ context: "web",
42
+ ...metrics,
43
+ ...loadTimes()
44
+ }
45
+ });
46
+ };
47
+ document.addEventListener("visibilitychange", onVisibilityChange);
48
+ }
49
+ /**
50
+ * Returns the DOMContentLoaded and load times reached so far.
51
+ * @returns the times in ms, as `domLoad` and `pageLoad`
52
+ */
53
+ function loadTimes() {
54
+ const [navigation] = performance.getEntriesByType("navigation");
55
+ const times = {};
56
+ if (navigation?.domContentLoadedEventEnd > 0) times.domLoad = navigation.domContentLoadedEventEnd;
57
+ if (navigation?.loadEventEnd > 0) times.pageLoad = navigation.loadEventEnd;
58
+ return times;
59
+ }
60
+ //#endregion
61
+ export { webVitals };
62
+
63
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/web-vitals.const.ts","../src/web-vitals.ts"],"sourcesContent":["export const PERFORMANCE_EVENT_TYPE = 'Performance'\nexport const WEB_CONTEXT = 'web'\nexport const WEB_VITALS_PLUGIN_NAME = 'web-vitals'\n","import type { Client, StatsPlugin } from '@codeserk/forge-stats'\nimport { type Metric, onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals'\n\nimport { PERFORMANCE_EVENT_TYPE, WEB_CONTEXT, WEB_VITALS_PLUGIN_NAME } from './web-vitals.const'\nimport type { WebVitalsOptions } from './web-vitals.types'\n\n/**\n * Returns the plugin that tracks the web vitals of the page load as one `Performance` event.\n * @param options\n * @returns the plugin for `init`\n */\nexport function webVitals(options: WebVitalsOptions): StatsPlugin {\n return {\n name: WEB_VITALS_PLUGIN_NAME,\n setup: (client) => captureWebVitals(client, options),\n }\n}\n\n/**\n * Tracks the web vitals through the client the first time the page is hidden, when INP and CLS are final.\n * @param client\n * @param options\n */\nfunction captureWebVitals(client: Client, { getRoute }: WebVitalsOptions): void {\n const metrics: Record<string, number> = {}\n const collect = (metric: Metric) => {\n metrics[metric.name.toLowerCase()] = metric.value\n }\n onLCP(collect)\n onINP(collect)\n onCLS(collect)\n onFCP(collect)\n onTTFB(collect)\n\n const onVisibilityChange = () => {\n if (document.visibilityState !== 'hidden') {\n return\n }\n\n document.removeEventListener('visibilitychange', onVisibilityChange)\n client.track({\n type: PERFORMANCE_EVENT_TYPE,\n name: getRoute(),\n data: { context: WEB_CONTEXT, ...metrics, ...loadTimes() },\n })\n }\n document.addEventListener('visibilitychange', onVisibilityChange)\n}\n\n/**\n * Returns the DOMContentLoaded and load times reached so far.\n * @returns the times in ms, as `domLoad` and `pageLoad`\n */\nfunction loadTimes(): Record<string, number> {\n const [navigation] = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[]\n const times: Record<string, number> = {}\n if (navigation?.domContentLoadedEventEnd > 0) {\n times.domLoad = navigation.domContentLoadedEventEnd\n }\n if (navigation?.loadEventEnd > 0) {\n times.pageLoad = navigation.loadEventEnd\n }\n\n return times\n}\n"],"mappings":";;;AAAA,MAAa,yBAAyB;AAEtC,MAAa,yBAAyB;;;;;;;;ACStC,SAAgB,UAAU,SAAwC;AAChE,QAAO;EACL,MAAM;EACN,QAAQ,WAAW,iBAAiB,QAAQ,QAAQ;EACrD;;;;;;;AAQH,SAAS,iBAAiB,QAAgB,EAAE,YAAoC;CAC9E,MAAM,UAAkC,EAAE;CAC1C,MAAM,WAAW,WAAmB;AAClC,UAAQ,OAAO,KAAK,aAAa,IAAI,OAAO;;AAE9C,OAAM,QAAQ;AACd,OAAM,QAAQ;AACd,OAAM,QAAQ;AACd,OAAM,QAAQ;AACd,QAAO,QAAQ;CAEf,MAAM,2BAA2B;AAC/B,MAAI,SAAS,oBAAoB,SAC/B;AAGF,WAAS,oBAAoB,oBAAoB,mBAAmB;AACpE,SAAO,MAAM;GACX,MAAM;GACN,MAAM,UAAU;GAChB,MAAM;IAAE,SAAA;IAAsB,GAAG;IAAS,GAAG,WAAW;IAAE;GAC3D,CAAC;;AAEJ,UAAS,iBAAiB,oBAAoB,mBAAmB;;;;;;AAOnE,SAAS,YAAoC;CAC3C,MAAM,CAAC,cAAc,YAAY,iBAAiB,aAAa;CAC/D,MAAM,QAAgC,EAAE;AACxC,KAAI,YAAY,2BAA2B,EACzC,OAAM,UAAU,WAAW;AAE7B,KAAI,YAAY,eAAe,EAC7B,OAAM,WAAW,WAAW;AAG9B,QAAO"}
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@codeserk/forge-stats-web-vitals",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.0",
4
+ "description": "Web vitals plugin for Forge Stats. Tracks the web performance of every page load as one Performance event.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "forge",
8
+ "analytics",
9
+ "stats",
10
+ "performance",
11
+ "web-vitals"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.mjs",
18
+ "types": "./dist/index.d.cts",
19
+ "exports": {
20
+ ".": {
21
+ "import": {
22
+ "types": "./dist/index.d.mts",
23
+ "default": "./dist/index.mjs"
24
+ },
25
+ "require": {
26
+ "types": "./dist/index.d.cts",
27
+ "default": "./dist/index.cjs"
28
+ }
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsdown",
38
+ "dev": "tsdown --watch",
39
+ "test": "jest",
40
+ "lint": "eslint src",
41
+ "version:patch": "npm version patch",
42
+ "version:minor": "npm version minor",
43
+ "version:major": "npm version major",
44
+ "publish:dry": "npm publish --dry-run",
45
+ "publish:npm": "npm run build && npm publish"
46
+ },
47
+ "peerDependencies": {
48
+ "@codeserk/forge-stats": "^0.4.0"
49
+ },
50
+ "dependencies": {
51
+ "web-vitals": "^6.2.3"
52
+ },
53
+ "devDependencies": {
54
+ "@types/jest": "^30.0.0",
55
+ "jest": "^30.3.0",
56
+ "ts-jest": "^29.4.6",
57
+ "tsdown": "^0.21.4",
58
+ "typescript": "^5.7.0"
59
+ }
60
+ }