This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking-client-js.git


The following commit(s) were added to refs/heads/master by this push:
     new 86cf96f  feat: disconnect observers and add new URLs to report for web 
vitals (#142)
86cf96f is described below

commit 86cf96f0a4d18334e8fd75e864e82aba95bec825
Author: Fine0830 <[email protected]>
AuthorDate: Fri Sep 27 14:23:16 2024 +0800

    feat: disconnect observers and add new URLs to report for web vitals (#142)
---
 README.md                      |  7 ++--
 src/monitor.ts                 |  9 ++--
 src/performance/index.ts       | 93 +++++++++++++++++++++++++++---------------
 src/performance/type.ts        |  8 +++-
 src/services/constant.ts       |  2 +
 src/services/eventsListener.ts | 24 +++++++++++
 src/services/report.ts         | 22 +++++-----
 src/types.ts                   |  1 -
 test/docker/Dockerfile.test-ui |  2 +-
 test/docker/index.js           |  1 -
 test/env                       |  2 +-
 11 files changed, 114 insertions(+), 57 deletions(-)

diff --git a/README.md b/README.md
index 4366943..0988862 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ The register method supports the following parameters.
 |jsErrors|Boolean|Support js errors monitoring|false|true|
 |apiErrors|Boolean|Support API errors monitoring|false|true|
 |resourceErrors|Boolean|Support resource errors monitoring|false|true|
-|useFmp|Boolean|Collect FMP (first meaningful paint) data of the first 
screen|false|false|
+|useFmp|Boolean|Collect FMP (first meaningful paint) data of the first screen. 
Deprecated: This is no longer recommended. Please use the useWebVitals instead. 
|false|false|
 |enableSPA|Boolean|Monitor the page hashchange event and report PV, which is 
suitable for [single page application 
scenarios](https://github.com/apache/skywalking-client-js#spa-page). 
|false|false|
 |autoTracePerf|Boolean|Support sending of performance data 
automatically.|false|true|
 |vue|Vue|Support vue2 errors monitoring. Deprecated: This is no longer 
recommended. Please use the [Catching errors in 
frames](https://github.com/apache/skywalking-client-js#catching-errors-in-frames-including-react-angular-vue)
 scenario instead. |false|undefined|
@@ -61,6 +61,7 @@ The register method supports the following parameters.
 |noTraceOrigins|(string \| RegExp)[]|Origin in the `noTraceOrigins` list will 
not be traced.|false|[]|
 |traceTimeInterval|Number|Support setting time interval to report 
segments.|false|60000|
 |customTags|Array|Custom Tags|false|-|
+|useWebVitals|Boolean|Collect three core web vitals|false|false|
 
 ## Collect Metrics Manually
 Use the `setPerformance` method to report metrics at the moment of page loaded 
or any other moment meaningful.
@@ -77,7 +78,7 @@ ClientMonitor.setPerformance({
   service: 'browser-app',
   serviceVersion: '1.0.0',
   pagePath: location.href,
-  useFmp: true
+  useWebVitals: true
 });
 ```
 
@@ -100,7 +101,7 @@ app.on('routeChange', function (next) {
     service: 'browser-app',
     serviceVersion: '1.0.0',
     pagePath: location.href,
-    useFmp: true
+    useWebVitals: true
   });
 });   
 ```
diff --git a/src/monitor.ts b/src/monitor.ts
index 89a7a58..01e8090 100644
--- a/src/monitor.ts
+++ b/src/monitor.ts
@@ -27,7 +27,7 @@ const ClientMonitor = {
     apiErrors: true,
     resourceErrors: true,
     autoTracePerf: true, // trace performance detail
-    useFmp: false, // use first meaningful paint
+    useWebVitals: false,
     enableSPA: false,
     traceSDKInternal: false,
     detailMode: true,
@@ -84,7 +84,6 @@ const ClientMonitor = {
     this.customOptions = {
       ...this.customOptions,
       ...configs,
-      useFmp: false,
     };
     this.validateOptions();
     this.performance(this.customOptions);
@@ -139,7 +138,7 @@ const ClientMonitor = {
       apiErrors,
       resourceErrors,
       autoTracePerf,
-      useFmp,
+      useWebVitals,
       enableSPA,
       traceSDKInternal,
       detailMode,
@@ -173,8 +172,8 @@ const ClientMonitor = {
     if (typeof autoTracePerf !== 'boolean') {
       this.customOptions.autoTracePerf = true;
     }
-    if (typeof useFmp !== 'boolean') {
-      this.customOptions.useFmp = false;
+    if (typeof useWebVitals !== 'boolean') {
+      this.customOptions.useWebVitals = false;
     }
     if (typeof enableSPA !== 'boolean') {
       this.customOptions.enableSPA = false;
diff --git a/src/performance/index.ts b/src/performance/index.ts
index f750270..2ce3baa 100644
--- a/src/performance/index.ts
+++ b/src/performance/index.ts
@@ -17,15 +17,30 @@
 
 import {CustomOptionsType} from '../types';
 import Report from '../services/report';
-import {prerenderChangeListener} from "../services/eventsListener";
+import {prerenderChangeListener, onHidden, runOnce, idlePeriod} from 
"../services/eventsListener";
 import pagePerf from './perf';
 import FMP from './fmp';
 import {observe} from "../services/observe";
-import {LCPMetric, FIDMetric} from "./type";
+import {LCPMetric, FIDMetric, CLSMetric} from "./type";
 import {LayoutShift} from "../services/types";
 import {getVisibilityObserver} from '../services/getVisibilityObserver';
 import {getActivationStart} from '../services/getNavigationEntry';
 
+const handler = {
+  set(target: {[key: string]: unknown}, prop: string, value: unknown) {
+    target[prop] = value;
+    const source: {[key: string]: unknown} = {
+      ...target,
+      collector: undefined,
+      useWebVitals: undefined,
+    };
+    if (target.useWebVitals && !isNaN(Number(target.fmpTime)) && 
!isNaN(Number(target.lcpTime)) && !isNaN(Number(target.clsTime))) {
+      new TracePerf().reportPerf(source, String(target.collector));
+    }
+    return true;
+  }
+};
+const reportedMetricNames: Record<string, boolean> = {};
 class TracePerf {
   private options: CustomOptionsType = {
     pagePath: '',
@@ -34,7 +49,7 @@ class TracePerf {
     collector: ''
   };
   private perfInfo = {};
-  private coreWebMetrics: {[key: string]: string | number | undefined} = {};
+  private coreWebMetrics: Record<string, unknown> = {};
   public getPerf(options: CustomOptionsType) {
     this.options = options;
     this.perfInfo = {
@@ -42,7 +57,7 @@ class TracePerf {
       serviceVersion: options.serviceVersion,
       service: options.service,
     }
-    this.coreWebMetrics = new Proxy({...this.perfInfo, collector: 
options.collector}, handler);
+    this.coreWebMetrics = new Proxy({...this.perfInfo, collector: 
options.collector, useWebVitals: options.useWebVitals}, handler);
     // trace and report perf data and pv to serve when page loaded
     if (document.readyState === 'complete') {
       this.getBasicPerf();
@@ -63,8 +78,6 @@ class TracePerf {
       this.LCP();
       this.FID();
       this.CLS();
-    }
-    if (this.options.useFmp) {
       const {fmpTime} = await new FMP();
       this.coreWebMetrics.fmpTime = Math.floor(fmpTime);
     }
@@ -86,11 +99,10 @@ class TracePerf {
             entry.startTime - firstEntry.startTime < 5000
           ) {
             partValue += entry.value;
-            entryList.push(entry);
           } else {
             partValue = entry.value;
-            entryList = [entry];
           }
+          entryList.push(entry);
         }
       });
       if (partValue > clsTime) {
@@ -98,7 +110,15 @@ class TracePerf {
       }
     };
 
-    observe('layout-shift', handleEntries);
+    const obs = observe('layout-shift', handleEntries);
+
+    if (!obs) {
+      return;
+    }
+    onHidden(() => {
+      handleEntries(obs.takeRecords() as CLSMetric['entries']);
+      obs!.disconnect();
+    });
   }
   private LCP() {
     prerenderChangeListener(() => {
@@ -112,7 +132,21 @@ class TracePerf {
         }
       };
   
-     observe('largest-contentful-paint', processEntries);
+      const obs = observe('largest-contentful-paint', processEntries);
+      if (!obs) {
+        return;
+      }
+      const disconnect = runOnce(() => {
+        if (!reportedMetricNames['lcp']) {
+          processEntries(obs!.takeRecords() as LCPMetric['entries']);
+          obs!.disconnect();
+          reportedMetricNames['lcp'] = true;
+        }
+      });
+      ['keydown', 'click'].forEach((type) => {
+        addEventListener(type, () => idlePeriod(disconnect), true);
+      });
+      onHidden(disconnect);
     })
   }
   private FID() {
@@ -126,15 +160,24 @@ class TracePerf {
             fidTime,
             ...this.perfInfo,
           };
-          this.reportPerf(perfInfo);
+          new Report('WEBINTERACTION', 
this.options.collector).sendByXhr(perfInfo);
         }
       };
   
       const processEntries = (entries: FIDMetric['entries']) => {
         entries.forEach(processEntry);
       };
-  
-      observe('first-input', processEntries);
+      const obs = observe('first-input', processEntries);
+      if (!obs) {
+        return;
+      }
+
+      onHidden(
+        runOnce(() => {
+          processEntries(obs.takeRecords() as FIDMetric['entries']);
+          obs.disconnect();
+        }),
+      );
     })
   }
   private getBasicPerf() {
@@ -144,17 +187,17 @@ class TracePerf {
       ...perfDetail,
       ...this.perfInfo,
     };
-    this.reportPerf({...perfInfo, isPV: true});
+    new Report('PERF', this.options.collector).sendByXhr(perfInfo);
+    // clear perf data
+    this.clearPerf();
   }
 
-  public reportPerf(data: {[key: string]: number | string | boolean}, 
collector?: string) {
+  public reportPerf(data: {[key: string]: unknown}, collector: string) {
     const perf = {
       ...data,
       ...this.perfInfo
     };
-    new Report('PERF', collector || this.options.collector).sendByXhr(perf);
-    // clear perf data
-    this.clearPerf();
+    new Report('WEBVITALS', collector).sendByXhr(perf);
   }
 
   private clearPerf() {
@@ -166,17 +209,3 @@ class TracePerf {
 }
 
 export default new TracePerf();
-
-const handler = {
-  set(target: {[key: string]: number | string | undefined}, prop: string, 
value: number | string | undefined) {
-    target[prop] = value;
-    if (!isNaN(Number(target.fmpTime)) && !isNaN(Number(target.lcpTime)) && 
!isNaN(Number(target.clsTime))) {
-      const source: {[key: string]: number | string | undefined} = {
-        ...target,
-        collector: undefined,
-      };
-      new TracePerf().reportPerf(source, String(target.collector));
-    }
-    return true;
-  }
-};
diff --git a/src/performance/type.ts b/src/performance/type.ts
index 81d06b6..ade739c 100644
--- a/src/performance/type.ts
+++ b/src/performance/type.ts
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {LargestContentfulPaint} from "../services/types";
+import {LargestContentfulPaint, LayoutShift} from "../services/types";
 export interface ICalScore {
   dpss: ICalScore[];
   st: number;
@@ -50,4 +50,8 @@ export interface LCPMetric {
 export interface FIDMetric {
   name: 'FID';
   entries: PerformanceEventTiming[];
-}
\ No newline at end of file
+}
+export interface CLSMetric {
+  name: 'CLS';
+  entries: LayoutShift[];
+}
diff --git a/src/services/constant.ts b/src/services/constant.ts
index d30d326..9556f7b 100644
--- a/src/services/constant.ts
+++ b/src/services/constant.ts
@@ -31,6 +31,8 @@ export enum ReportTypes {
   ERROR = '/browser/errorLog',
   ERRORS = '/browser/errorLogs',
   PERF = '/browser/perfData',
+  WEBVITALS = '/browser/perfData/webVitals',
+  WEBINTERACTION = '/browser/perfData/webInteraction',
   SEGMENT = '/v3/segment',
   SEGMENTS = '/v3/segments',
 }
diff --git a/src/services/eventsListener.ts b/src/services/eventsListener.ts
index 5c863d1..b7ebaf3 100644
--- a/src/services/eventsListener.ts
+++ b/src/services/eventsListener.ts
@@ -29,3 +29,27 @@ export function onHidden (cb: () => void) {
     }
   });
 };
+
+export function runOnce (callback: () => void) {
+  let called = false;
+  return () => {
+    if (!called) {
+      callback();
+      called = true;
+    }
+  };
+};
+
+export function idlePeriod(callback: () => void): number {
+  const func = window.requestIdleCallback || window.setTimeout;
+
+  let handle = -1;
+  callback = runOnce(callback);
+  if (document.visibilityState === 'hidden') {
+    callback();
+  } else {
+    handle = func(callback);
+    onHidden(callback);
+  }
+  return handle;
+};
diff --git a/src/services/report.ts b/src/services/report.ts
index 3a7cff2..2a3028c 100644
--- a/src/services/report.ts
+++ b/src/services/report.ts
@@ -19,17 +19,17 @@ class Report {
   private url: string = '';
 
   constructor(type: string, collector: string) {
-    if (type === 'ERROR') {
-      this.url = collector + ReportTypes.ERROR;
-    } else if (type === 'ERRORS') {
-      this.url = collector + ReportTypes.ERRORS;
-    } else if (type === 'SEGMENT') {
-      this.url = collector + ReportTypes.SEGMENT;
-    } else if (type === 'SEGMENTS') {
-      this.url = collector + ReportTypes.SEGMENTS;
-    } else if (type === 'PERF') {
-      this.url = collector + ReportTypes.PERF;
-    }
+    const typesMap: Record<string, string> = {
+      ERROR: ReportTypes.ERROR,
+      ERRORS: ReportTypes.ERRORS,
+      SEGMENT: ReportTypes.SEGMENT,
+      SEGMENTS: ReportTypes.SEGMENTS,
+      PERF: ReportTypes.PERF,
+      WEBVITALS: ReportTypes.WEBVITALS,
+      WEBINTERACTION: ReportTypes.WEBINTERACTION,
+    };
+
+    this.url = `${collector}${typesMap[type]}`;
   }
 
   public sendByFetch(data: any) {
diff --git a/src/types.ts b/src/types.ts
index 9c6eb3e..353bc50 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -20,7 +20,6 @@ export interface CustomOptionsType extends 
CustomReportOptions {
   apiErrors?: boolean;
   resourceErrors?: boolean;
   autoTracePerf?: boolean;
-  useFmp?: boolean;
   enableSPA?: boolean;
   vue?: any;
   traceSDKInternal?: boolean;
diff --git a/test/docker/Dockerfile.test-ui b/test/docker/Dockerfile.test-ui
index ecbb0fe..e19c8f0 100644
--- a/test/docker/Dockerfile.test-ui
+++ b/test/docker/Dockerfile.test-ui
@@ -13,7 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-FROM node:10.23 AS builder
+FROM node:18.12 AS builder
 
 ADD . /skywalking-client-js
 WORKDIR /skywalking-client-js
diff --git a/test/docker/index.js b/test/docker/index.js
index eae9167..73220e0 100644
--- a/test/docker/index.js
+++ b/test/docker/index.js
@@ -22,7 +22,6 @@ ClientMonitor.register({
   pagePath: 'index.html',
   serviceVersion: 'v1.0.0',
   vue: Vue,
-  useFmp: true,
   traceTimeInterval: 2000,
 });
 
diff --git a/test/env b/test/env
index 4cf6a6b..4fd01b7 100644
--- a/test/env
+++ b/test/env
@@ -14,6 +14,6 @@
 # limitations under the License.
 
 SW_AGENT_PYTHON_COMMIT=c8479000eb729cc86509222fd48b942edcaaca74
-SW_AGENT_CLIENT_JS_TEST_COMMIT=4f1eb1dcdbde3ec4a38534bf01dded4ab5d2f016
+SW_AGENT_CLIENT_JS_TEST_COMMIT=d144d7713b84a00bc8f1a1ecb16fdc41ce657eef
 
 SW_CTL_COMMIT=d2f1cff71f3ea9f325ff1c0d99dd0c40a35e527c

Reply via email to