Frontend Development 31 min read

Building a Frontend Monitoring SDK from Scratch: Design, Implementation, and Performance Metrics

This article explains how to design and implement a complete front‑end monitoring SDK, covering stability, user‑experience, and business‑expansion scenarios, with step‑by‑step guidance on SDK architecture, Rollup build setup, error capturing, data reporting, performance measurement, and advanced topics such as source‑map handling and error aggregation.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Building a Frontend Monitoring SDK from Scratch: Design, Implementation, and Performance Metrics

The article introduces front‑end monitoring as an essential part of large projects, highlighting its benefits for stability, user experience, and business expansion.

It outlines the overall architecture: a client‑side SDK that collects logs, a log server that receives and stores them, and a visualization platform that displays the data.

1. SDK Design and Build – The SDK is packaged as a <script> file using Rollup. The guide shows how to create the project directory, install dependencies, and configure rollup.config.js :

mkdir monitor-sdk
cd monitor-sdk
npm init -y
npm install rollup rollup-plugin-terser @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-babel typescript @babel/preset-typescript @babel/preset-env @babel/core @babel/cli -D
tsc --init
touch index.ts
touch rollup.config.js
// rollup.config.js
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import { babel } from "@rollup/plugin-babel";
import { terser } from "rollup-plugin-terser";
import path from "path";
const extensions = [".ts", ".tsx", ".js", ".jsx"];
export default () => ({
  input: path.resolve(__dirname, "index.ts"),
  output: { file: path.resolve(__dirname, "dist/myMonitor.js"), format: "umd", name: "myMonitor" },
  plugins: [resolve({ extensions }), commonjs(), babel({ extensions, presets: ["@babel/preset-env", ["@babel/preset-typescript", { isTSX: true, allExtensions: true }]], babelHelpers: "bundled" }), terser()]
});

Adding a build script to package.json enables npm run build to produce dist/myMonitor.js .

2. Error Monitoring – The SDK registers global listeners for window.addEventListener('error') to capture uncaught JavaScript errors, formats the stack trace, and builds an ErrorLog object. It also tracks the last user interaction to record a CSS selector path.

// modules/jsError.ts
export default function injectJSError() {
  window.addEventListener("error", event => {
    const lastEvent = getLastEvent();
    const errorLog = {
      type: "error",
      errorType: "jsError",
      message: event.message,
      filename: event.filename,
      position: `${event.lineno}:${event.colno}`,
      stack: formatStack(event.error.stack),
      selector: lastEvent ? getSelector() : ""
    };
    tracker.send(errorLog);
  }, true);
}

Promise rejections are captured via the unhandledrejection event, distinguishing between actual code errors and explicit reject() calls.

// modules/jsError.ts (continued)
window.addEventListener("unhandledrejection", event => {
  const reason = event.reason;
  // build appropriate ErrorLog based on reason type
});

Resource loading errors (JS, CSS, images) are detected by inspecting event.target in the same error handler and reporting a loadResourceError log.

3. Data Reporting – Three reporting methods are described: traditional AJAX/XHR, image‑based GET requests (GIF beacon), and navigator.sendBeacon() . A Tracker class builds the final payload, adds base device info (browser, OS, etc.) via ua-parser-js , and sends it.

// utils/tracker.ts
class Tracker {
  url = "http://localhost:8080/send/monitor.gif";
  send(data) {
    const baseData = getLogBaseData();
    const log = { baseLog: baseData, ...data };
    const img = new Image();
    img.src = `${this.url}?data=${encodeURIComponent(JSON.stringify(log))}`;
  }
}
export default new Tracker();

4. API Request Monitoring – By monkey‑patching XMLHttpRequest.prototype methods ( open , setRequestHeader , send ), the SDK records request method, URL, headers, duration, and response status, reporting errors for status ≥ 400 or network failures.

// modules/xhr.ts
const oldOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async) {
  this.logData = { method: method.toUpperCase(), url };
  return oldOpen.apply(this, arguments);
};
// similar overrides for setRequestHeader and send with reporting logic

5. White‑Screen Detection – After an error, the SDK samples points across the viewport using document.elementFromPoint to determine if only wrapper elements ( html , body , #root ) are present, indicating a white screen.

// utils/checkWhiteScreen.ts
export default function checkWhiteScreen() {
  const wrapperElements = ["html", "body", "#root"];
  let emptyPoints = 0;
  // sample 9 points horizontally and vertically, count wrapper hits
  return emptyPoints === 18;
}

6. Performance Metrics – The SDK measures page load times (fetch start, DOMContentLoaded, load) using PerformanceNavigationTiming (fallback to performance.timing ), and captures paint metrics (FP, FCP, FMP, LCP) via PerformanceObserver . After page load, it waits 3 seconds and reports the collected paint data.

// modules/paint.ts
if (PerformanceObserver) {
  let FP, FCP, FMP, LCP;
  const observer = new PerformanceObserver(list => {
    // handle paint entries
  });
  observer.observe({ entryTypes: ["paint", "element", "largest-contentful-paint"] });
}
window.addEventListener("load", () => {
  setTimeout(() => {
    observer.disconnect();
    tracker.send({ type: "paint", FP: FP?.startTime, FCP: FCP?.startTime, FMP: FMP?.startTime, LCP: LCP?.startTime });
  }, 3000);
});

7. Long‑Task (Jank) Monitoring – Using PerformanceObserver with entryTypes: ["longtask"] , tasks longer than 100 ms are logged with start time, duration, and the last user interaction selector.

// modules/longTask.ts
if (PerformanceObserver) {
  const observer = new PerformanceObserver(list => {
    list.getEntries().forEach(entry => {
      if (entry.duration > 100) {
        const log = { type: "longTask", startTime: entry.startTime, duration: entry.duration, selector: getLastEvent() ? getSelector() : "" };
        tracker.send(log);
      }
    });
  });
  observer.observe({ entryTypes: ["longtask"] });
}

8. Advanced Topics – The article discusses source‑map based error localization on the server side, error aggregation by hashing name + message + stack , server‑side data cleaning, and alerting mechanisms (email, instant‑messaging) for new errors or threshold breaches.

frontendMonitoringperformancesdkJavaScripterror handling
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.