diff --git a/.eslint.config.js b/.eslint.config.js index cd29ffa..d304f11 100644 --- a/.eslint.config.js +++ b/.eslint.config.js @@ -14,17 +14,17 @@ module.exports = { "plugin:prettier/recommended", ], rules: { - // TypeScript 规则 + // TypeScript rules "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-floating-promises": "error", - // ESLint 基础规则 + // ESLint basic rules "no-console": "warn", eqeqeq: ["error", "always"], - // Prettier 集成 (通过 plugin:prettier/recommended 已处理) + // Prettier integration (handled by plugin:prettier/recommended) }, ignorePatterns: ["dist/**", "node_modules/**"], }; diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6257b0d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,44 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Common Commands + +- **Build the project:** `npm run build` + - Compiles the TypeScript source code in `src/` to JavaScript in `dist/`. + +- **Run tests:** `npm test` + - Executes the test suite using Jest. Test files are located alongside the source files. + +- **Lint the code:** `npm run lint` + - Checks the TypeScript files for code quality and style issues using ESLint. + +- **Format the code:** `npm run format` + - Automatically formats the code using Prettier. + +## High-level Architecture + +This repository contains the source code for the Softprobe Web SDK, a library designed to be integrated into web applications to capture and report user interactions and performance metrics. The core functionality is built on top of the **OpenTelemetry** framework. + +### Initialization Flow + +1. The entry point for the SDK is `src/index.ts`, which exports the `initInspector` function. +2. `initInspector` calls `initBrowserInspector` in `src/browser.ts`, which contains the main initialization logic. +3. `initBrowserInspector` sets up the OpenTelemetry `WebTracerProvider`, configures span processors (a `CustomConsoleSpanExporter` for development and an `OTLPTraceExporter` to send data to a collector endpoint), and creates a user resource with metadata like API key, user ID, and a session ID. +4. It registers a series of auto-instrumentations provided by `@opentelemetry/auto-instrumentations-web` to automatically capture events like user interactions (clicks, scrolls), fetch/XHR requests, etc. + +### Data Collection + +The SDK collects a wide range of data, which can be categorized as follows: + +1. **Environment Information (`src/environment-recorder.ts`):** + - On initialization, it records static information about the user's environment, such as user agent, screen resolution, timezone, and URL parameters (e.g., UTM tags). + - It also records page load performance metrics from `window.performance.timing`. + +2. **Automatic Event Listening (`src/event-listeners.ts`):** + - This file sets up a comprehensive set of global event listeners to capture user behavior automatically. + - Events captured include: page unload, page zoom, scroll, mouse movement, hover, drag-and-drop, keyboard shortcuts, form resets, and SPA route changes (by wrapping `history.pushState` and `history.replaceState`). + - Most of these listeners call corresponding functions in `src/environment-recorder.ts` to create and send OpenTelemetry spans for each action. + +3. **Session Management (`src/SessionManager.ts`):** + - A unique session ID (`spSessionId`) is generated and stored in `sessionStorage` to group all events from a single user session. This ID is added as a custom HTTP header (`x-sp-session-id`) to outgoing requests via a custom propagator (`src/HttpHeaderPropagator.ts`). diff --git a/README.md b/README.md index bd49c59..7b9bbff 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,93 @@ -# @softprobe/web-inspector +# Web SDK (`@softprobe/web-inspector`) -## 安装 +This guide covers the installation and usage of the Softprobe Web SDK. + +## Features + +The Softprobe Web SDK is designed to provide comprehensive insights into your web application's performance and user behavior. Key features include: + +- **Automatic Performance Monitoring**: Automatically captures and reports key page load performance metrics. +- **User Interaction Tracking**: Records user interactions such as clicks, scrolls, and form submissions to help you understand user journeys. +- **Network Request Tracing**: Monitors all `fetch` and `XMLHttpRequest` requests to identify slow or failing API calls. +- **Environment and Session Recording**: Gathers valuable context by recording browser, OS, and device information, and groups all events within a single user session. +- **Custom Instrumentation**: Provides a simple API to create custom spans for tracing specific business logic or user interactions. + +## Installation + +Install the package using your preferred package manager: ```bash npm install @softprobe/web-inspector ``` -## 使用 +## Usage + +### Initialization -### 初始化配置 +Initialize the inspector in your web application's entry point. ```typescript import { initInspector } from "@softprobe/web-inspector"; +// Only need to call register once export function register() { - // 客户端初始化 + // Initialize the client initInspector({ - apiKey: "YOUR_API_KEY", - userId: "YOUR_USER_ID", - serviceName: "YOUR_BUSINESS_SOURCE", - // 上报数据地址: /v1/traces + apiKey: "", + userId: "", + serviceName: "YOUR_SERVICE_NAME", + // Data collector endpoint: /v1/traces collectorEndpoint: process.env.INSPECTOR_COLLECTOR_URL!, - // 开发环境自动启用Console日志 + // Automatically enables console logging in development env: "dev", - + // Optional: disable scroll observation observeScroll: false, }) .then(({ provider }) => { - console.log("Success"); + console.log("Softprobe inspector initialized successfully."); }) - .catch((err) => { - console.log("Failure"); + .catch((error) => { + console.error("Failed to initialize Softprobe inspector:", error); }); } ``` -### 手动创建 Span (示例) +### Creating Spans Manually + +You can create custom spans to trace specific business logic or user interactions. ```typescript -// pages/index.tsx +// Example in a React component (e.g., pages/index.tsx) import { trace } from '@softprobe/web-inspector'; export default function Home() { const handleClick = () => { + // Get a tracer instance const tracer = trace.getTracer('nextjs-tracer'); + + // Start a new span const span = tracer.startSpan('checkout_process'); try { - // 业务逻辑... + // Your business logic here... + // Example: processing items in a shopping cart + + // Add attributes to the span for context span.setAttribute('item_count', 3); + span.setAttribute('user_tier', 'gold'); + + // Set the span status to OK on success span.setStatus({ code: trace.SpanStatusCode.OK }); + } catch (error) { + // Set the span status to ERROR on failure span.setStatus({ code: trace.SpanStatusCode.ERROR, message: error.message }); + } finally { + // End the span to record it span.end(); } }; diff --git a/src/CustomConsoleSpanExporter.ts b/src/CustomConsoleSpanExporter.ts index 73f5886..53cd20f 100644 --- a/src/CustomConsoleSpanExporter.ts +++ b/src/CustomConsoleSpanExporter.ts @@ -1,4 +1,4 @@ -// 创建控制台导出器(用于开发环境) +// Create a console exporter (for development environment) export class CustomConsoleSpanExporter { export(spans: any, resultCallback: any) { // console.log(`🔍 Exporting ${spans.length} spans`); @@ -11,12 +11,12 @@ export class CustomConsoleSpanExporter { console.log(` Trace ID: ${span.spanContext().traceId}`); console.log(` Duration: ${span.duration?.[0] || 0}ms`); - // 显示资源属性 + // Display resource attributes if (span.resource && span.resource.attributes) { console.log(` 📋 Resource Attributes:`, JSON.stringify(span.resource.attributes, null, 2)); } - // 显示 span 属性 + // Display span attributes if (span.attributes && Object.keys(span.attributes).length > 0) { console.log(` 🏷️ Span Attributes:`, JSON.stringify(span.attributes, null, 2)); } diff --git a/src/SessionManager.ts b/src/SessionManager.ts index 71f4eb6..0521f5b 100644 --- a/src/SessionManager.ts +++ b/src/SessionManager.ts @@ -1,9 +1,9 @@ /** - * SessionId 管理器 - * 统一管理sessionId的生成和获取 + * SessionId Manager + * Manages the generation and retrieval of session Ids. */ -// 生成UUID函数 +// Function to generate a UUID function generateUUID(): string { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0; @@ -12,13 +12,13 @@ function generateUUID(): string { }); } -// 生成sessionId +// Generate a sessionId function generateSessionId(): string { const uuid = generateUUID(); return `sp-session-${uuid}`; } -// 获取sessionId(从sessionStorage或生成新的) +// Get the sessionId (from sessionStorage or generate a new one) export function getSessionId(): string { if (typeof window === "undefined") return ""; @@ -33,7 +33,7 @@ export function getSessionId(): string { return sessionId; } -// 重置sessionId(清除当前的并生成新的) +// Reset the sessionId (clear the current one and generate a new one) export function resetSessionId(): string { if (typeof window === "undefined") return ""; @@ -44,7 +44,7 @@ export function resetSessionId(): string { return newSessionId; } -// 获取当前sessionId(不生成新的) +// Get the current sessionId (without generating a new one) export function getCurrentSessionId(): string | null { if (typeof window === "undefined") return null; return sessionStorage.getItem("x-sp-session-id"); diff --git a/src/browser.ts b/src/browser.ts index 2086fbe..4bec51d 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -26,7 +26,7 @@ export function initBrowserInspector(config: InspectorConfig): Promise<{ console.log("🚀 Starting OpenTelemetry initialization..."); const spSessionId = getSessionId(); - // 构造processor + // Construct processor const spanProcessors: SpanProcessor[] = []; if (config.env === "dev") { spanProcessors.push(new SimpleSpanProcessor(new CustomConsoleSpanExporter())); @@ -41,7 +41,7 @@ export function initBrowserInspector(config: InspectorConfig): Promise<{ ); } - // 构造resource + // Construct resource const resource = createUserResource({ apiKey: config.apiKey, userId: config.userId, @@ -49,14 +49,14 @@ export function initBrowserInspector(config: InspectorConfig): Promise<{ spSessionId: spSessionId, }); - // 构造provider + // Construct provider const provider = new WebTracerProvider({ resource, spanProcessors, }); console.log("✅ WebTracerProvider created"); - // 注册 provider 和 context manager + // Register provider and context manager provider.register({ contextManager: new ZoneContextManager(), propagator: new CompositePropagator({ @@ -71,12 +71,12 @@ export function initBrowserInspector(config: InspectorConfig): Promise<{ }); console.log("✅ Provider registered with ZoneContextManager"); - // 注册自动检测 + // Register auto-instrumentations try { registerInstrumentations({ instrumentations: [ getWebAutoInstrumentations({ - // 启用所有自动检测,使用默认配置 + // Enable all auto-instrumentations with default configurations "@opentelemetry/instrumentation-user-interaction": { eventNames: [ "click", @@ -89,12 +89,12 @@ export function initBrowserInspector(config: InspectorConfig): Promise<{ "blur", ], }, - // 自定义 Fetch 检测 + // Custom Fetch instrumentation "@opentelemetry/instrumentation-fetch": { propagateTraceHeaderCorsUrls: [/.*/], applyCustomAttributesOnSpan: (span: any, request: any, result: any) => { try { - // 记录请求信息 + // Record request information if (typeof request === "string") { span.setAttribute("http.request.url", request); span.setAttribute("http.request.method", "GET"); @@ -102,7 +102,7 @@ export function initBrowserInspector(config: InspectorConfig): Promise<{ span.setAttribute("http.request.url", request.url); span.setAttribute("http.request.method", request.method); - // 记录请求头 + // Record request headers if (request.headers) { const headers = typeof request.headers.entries === "function" @@ -125,12 +125,12 @@ export function initBrowserInspector(config: InspectorConfig): Promise<{ } } - // 记录响应信息 + // Record response information if (result instanceof Response) { span.setAttribute("http.response.status", result.status); span.setAttribute("http.response.status_text", result.statusText); - // 记录响应头 + // Record response headers const responseHeaders = Object.fromEntries(result.headers.entries()); const importantResponseHeaders = [ "content-type", @@ -151,7 +151,7 @@ export function initBrowserInspector(config: InspectorConfig): Promise<{ Object.keys(responseHeaders).length, ); - // 记录响应体大小(不记录内容,避免隐私问题) + // Record response body size (do not record content to avoid privacy issues) if (result.headers.get("content-length")) { span.setAttribute( "http.response.body.size", @@ -187,9 +187,9 @@ export function initBrowserInspector(config: InspectorConfig): Promise<{ const loader = () => { setTimeout(() => { Promise.all([ - // 初始化环境信息 + // Initialize environment information import("./environment-recorder"), - // 初始化事件监听器 + // Initialize event listeners import("./event-listeners"), ]) .then(([{ recordEnvironmentInfo, recordPageLoadInfo }, { initializeEventListeners }]) => { @@ -205,18 +205,18 @@ export function initBrowserInspector(config: InspectorConfig): Promise<{ .catch((error) => { reject(error); }); - }, 1000); // 延迟1秒确保所有资源加载完成 + }, 1000); // Delay for 1 second to ensure all resources are loaded }; - // 自动记录环境信息和初始化事件监听器 + // Automatically record environment information and initialize event listeners if (typeof window !== "undefined") { - // 等待页面加载完成后记录环境信息 + // Record environment information after the page has loaded if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { loader(); }); } else { - // 页面已经加载完成 + // The page has already loaded loader(); } } diff --git a/src/config.ts b/src/config.ts index a0e7425..111f681 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,15 +1,15 @@ export type InspectorConfig = { - // 业务相关 + // Business related apiKey: string; userId: string; - /** 区分业务线 */ + /** Differentiate business lines */ serviceName: string; - /** 接收span的地址,POST /v1/traces */ + /** Address for receiving spans, POST /v1/traces */ collectorEndpoint?: string; /** 'dev' | 'prd' */ env?: string; - // 配置相关 - /** 是否开启滚动监听, 默认不开启 */ + // Configuration related + /** Whether to enable scroll listening, disabled by default */ observeScroll?: boolean; }; diff --git a/src/createUserResource.ts b/src/createUserResource.ts index 07cf52e..1d6b53f 100644 --- a/src/createUserResource.ts +++ b/src/createUserResource.ts @@ -7,14 +7,16 @@ type Options = { serviceName: string; spSessionId: string; }; -// 创建用户资源信息(Mock 数据) +// Create user resource information (Mock data) export function createUserResource({ apiKey, userId, serviceName, spSessionId }: Options) { - // 模拟用户信息 - 在实际应用中这些数据应该来自认证系统 + // Mock user information - in a real application, this data should come from an authentication system const mockUserInfo = { - email: "harry@example.com", + // TODO: + email: "john_doe@example.com", username: "john_doe", - apiKey, - userId, + apiKey: apiKey || '', + userId: userId || '', + spSessionId, userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "unknown", language: typeof navigator !== "undefined" ? navigator.language : "en-US", @@ -25,7 +27,7 @@ export function createUserResource({ apiKey, userId, serviceName, spSessionId }: }; const resource = resourceFromAttributes({ - // 用户特定属性 + // User-specific attributes "user.email": mockUserInfo.email, "user.username": mockUserInfo.username, "user.id": mockUserInfo.userId, diff --git a/src/environment-recorder.ts b/src/environment-recorder.ts index 208f5e6..9c8c45a 100644 --- a/src/environment-recorder.ts +++ b/src/environment-recorder.ts @@ -1,38 +1,38 @@ -// environment-recorder.ts - 环境信息记录器 +// environment-recorder.ts - Environment information recorder import { trace } from "@opentelemetry/api"; export function recordEnvironmentInfo(sessionId?: string) { const tracer = trace.getTracer("web-env"); tracer.startActiveSpan("session.env", (span) => { try { - // 浏览器与系统 + // Browser and system span.setAttribute("browser.user_agent", navigator.userAgent); - span.setAttribute("browser.platform", navigator.platform); // OS 信息 + span.setAttribute("browser.platform", navigator.platform); // OS information span.setAttribute("device.pixel_ratio", window.devicePixelRatio); - // 屏幕 & 视口 + // Screen & viewport span.setAttribute("screen.width", window.screen.width); span.setAttribute("screen.height", window.screen.height); span.setAttribute("viewport.width", window.innerWidth); span.setAttribute("viewport.height", window.innerHeight); - // 网络 & 位置 + // Network & location const conn = (navigator as any).connection; if (conn) { - span.setAttribute("network.effectiveType", conn.effectiveType); // wifi/4g/… + span.setAttribute("network.effectiveType", conn.effectiveType); // wifi/4g/... span.setAttribute("network.rtt", conn.rtt); } span.setAttribute("browser.timezone", Intl.DateTimeFormat().resolvedOptions().timeZone); span.setAttribute("browser.language", navigator.language); - // 会话信息 + // Session information span.setAttribute("page.url", location.href); span.setAttribute("page.referrer", document.referrer || "direct"); if (sessionId) { - span.setAttribute("session.id", sessionId); // 记得脱敏/加密 + span.setAttribute("session.id", sessionId); // Remember to anonymize/encrypt } - // 也可以解析 URL 中的 UTM 参数 + // You can also parse UTM parameters from the URL const urlParams = new URLSearchParams(location.search); ["utm_source", "utm_medium", "utm_campaign"].forEach((key) => { if (urlParams.get(key)) { @@ -50,12 +50,12 @@ export function recordEnvironmentInfo(sessionId?: string) { }); } -// 记录页面加载信息 +// Record page load information export function recordPageLoadInfo() { const tracer = trace.getTracer("web-page"); tracer.startActiveSpan("page.load", (span) => { try { - // 页面加载性能 + // Page load performance if (window.performance && window.performance.timing) { const timing = window.performance.timing; const loadTime = timing.loadEventEnd - timing.navigationStart; @@ -68,14 +68,14 @@ export function recordPageLoadInfo() { span.setAttribute("page.request_time", timing.responseEnd - timing.requestStart); } - // 页面信息 + // Page information span.setAttribute("page.title", document.title); span.setAttribute("page.url", window.location.href); span.setAttribute("page.path", window.location.pathname); span.setAttribute("page.search", window.location.search); span.setAttribute("page.hash", window.location.hash); - // 文档信息 + // Document information span.setAttribute("document.ready_state", document.readyState); span.setAttribute("document.character_set", document.characterSet); span.setAttribute("document.content_type", document.contentType); @@ -90,7 +90,7 @@ export function recordPageLoadInfo() { }); } -// 记录用户交互信息 +// Record user interaction information export function recordUserInteraction( action: string, target?: HTMLElement, @@ -99,18 +99,18 @@ export function recordUserInteraction( const tracer = trace.getTracer("web-interaction"); tracer.startActiveSpan(`user.interaction.${action}`, (span) => { try { - // 基础交互信息 + // Basic interaction information span.setAttribute("user.action", action); span.setAttribute("user.action.timestamp", new Date().toISOString()); - // 目标元素信息 + // Target element information if (target) { span.setAttribute("target.tag_name", target.tagName); span.setAttribute("target.id", target.id || ""); span.setAttribute("target.class_name", target.className || ""); span.setAttribute("target.text_content", target.textContent?.substring(0, 100) || ""); - // 位置信息 + // Position information const rect = target.getBoundingClientRect(); span.setAttribute("target.position.x", rect.x); span.setAttribute("target.position.y", rect.y); @@ -118,12 +118,12 @@ export function recordUserInteraction( span.setAttribute("target.size.height", rect.height); } - // 页面上下文 + // Page context span.setAttribute("page.url", window.location.href); span.setAttribute("page.scroll_y", window.scrollY); span.setAttribute("page.scroll_x", window.scrollX); - // 自定义详情 + // Custom details if (details) { Object.entries(details).forEach(([key, value]) => { span.setAttribute(`custom.${key}`, String(value)); @@ -140,7 +140,7 @@ export function recordUserInteraction( }); } -// 记录网络请求信息 +// Record network request information export function recordNetworkRequest( url: string, method: string, @@ -161,7 +161,7 @@ export function recordNetworkRequest( const tracer = trace.getTracer("web-network"); tracer.startActiveSpan("network.request", (span) => { try { - // 基础请求信息 + // Basic request information span.setAttribute("http.method", method); span.setAttribute("http.url", url); span.setAttribute("http.scheme", new URL(url).protocol.replace(":", "")); @@ -180,10 +180,10 @@ export function recordNetworkRequest( span.setAttribute("request.timestamp", new Date().toISOString()); span.setAttribute("page.url", window.location.href); - // 记录请求详情 + // Record request details if (requestData) { if (requestData.headers) { - // 记录重要的请求头 + // Record important request headers const importantHeaders = [ "content-type", "authorization", @@ -202,7 +202,7 @@ export function recordNetworkRequest( } if (requestData.body) { - // 记录请求体信息(不记录敏感内容) + // Record request body information (do not record sensitive content) const bodyStr = typeof requestData.body === "string" ? requestData.body @@ -210,7 +210,7 @@ export function recordNetworkRequest( span.setAttribute("request.body_size", bodyStr.length); span.setAttribute("request.has_body", true); - // 只记录非敏感请求体的前100个字符 + // Only record the first 100 characters of non-sensitive request bodies if ( bodyStr.length <= 100 && !bodyStr.toLowerCase().includes("password") && @@ -225,10 +225,10 @@ export function recordNetworkRequest( } } - // 记录响应详情 + // Record response details if (responseData) { if (responseData.headers) { - // 记录重要的响应头 + // Record important response headers const importantResponseHeaders = [ "content-type", "content-length", @@ -247,7 +247,7 @@ export function recordNetworkRequest( } if (responseData.body) { - // 记录响应体信息 + // Record response body information const bodyStr = typeof responseData.body === "string" ? responseData.body @@ -255,7 +255,7 @@ export function recordNetworkRequest( span.setAttribute("response.body_size", bodyStr.length); span.setAttribute("response.has_body", true); - // 只记录响应体的前200个字符 + // Only record the first 200 characters of the response body if (bodyStr.length <= 200) { span.setAttribute("response.body_preview", bodyStr); } else { @@ -287,7 +287,7 @@ export function recordNetworkRequest( }); } -// 记录页面卸载信息 +// Record page unload information export function recordPageUnload() { const tracer = trace.getTracer("web-page"); tracer.startActiveSpan("page.unload", (span) => { @@ -298,7 +298,7 @@ export function recordPageUnload() { span.setAttribute("page.scroll_x", window.scrollX); span.setAttribute("page.unload_timestamp", new Date().toISOString()); - // 记录页面停留时间 + // Record page stay time if (window.performance && window.performance.timing) { const timing = window.performance.timing; const stayTime = Date.now() - timing.loadEventEnd; @@ -315,7 +315,7 @@ export function recordPageUnload() { }); } -// 记录路由变化(SPA history API) +// Record route changes (SPA history API) export function recordRouteChange( fromUrl: string, toUrl: string, @@ -341,7 +341,7 @@ export function recordRouteChange( }); } -// 记录页面缩放 +// Record page zoom export function recordPageZoom(scale: number) { const tracer = trace.getTracer("web-viewport"); tracer.startActiveSpan("viewport.zoom", (span) => { @@ -361,27 +361,27 @@ export function recordPageZoom(scale: number) { }); } -// 记录鼠标移动轨迹(节流) +// Record mouse movement trajectory (throttled) let mouseMoveBuffer: Array<{ x: number; y: number; timestamp: number }> = []; let mouseMoveTimer: NodeJS.Timeout | null = null; let lastMouseMoveTime = 0; -const MOUSE_MOVE_THROTTLE_MS = 100; // 每100ms最多记录一次 -const MOUSE_MOVE_BATCH_SIZE = 10; // 每10个点或每500ms处理一次 +const MOUSE_MOVE_THROTTLE_MS = 100; // Record at most once every 100ms +const MOUSE_MOVE_BATCH_SIZE = 10; // Process every 10 points or every 500ms export function recordMouseMove(x: number, y: number) { // const now = Date.now(); - // // 节流:如果距离上次记录时间太短,直接返回 + // // Throttle: if the time since the last record is too short, return directly // if (now - lastMouseMoveTime < MOUSE_MOVE_THROTTLE_MS) { // return; // } // lastMouseMoveTime = now; // mouseMoveBuffer.push({ x, y, timestamp: now }); - // // 如果缓冲区达到批量大小,立即处理 + // // If the buffer reaches the batch size, process it immediately // if (mouseMoveBuffer.length >= MOUSE_MOVE_BATCH_SIZE) { // processMouseMoveBuffer(); // return; // } - // // 设置定时器,如果500ms内没有达到批量大小,也会处理 + // // Set a timer, if the batch size is not reached within 500ms, it will also be processed // if (!mouseMoveTimer) { // mouseMoveTimer = setTimeout(() => { // processMouseMoveBuffer(); @@ -408,7 +408,7 @@ function processMouseMoveBuffer() { ); span.setAttribute("page.url", window.location.href); - // 计算移动距离 + // Calculate movement distance let totalDistance = 0; for (let i = 1; i < mouseMoveBuffer.length; i++) { const dx = mouseMoveBuffer[i].x - mouseMoveBuffer[i - 1].x; @@ -428,7 +428,7 @@ function processMouseMoveBuffer() { } }); - // 清理缓冲区 + // Clear buffer mouseMoveBuffer = []; if (mouseMoveTimer) { clearTimeout(mouseMoveTimer); @@ -436,7 +436,7 @@ function processMouseMoveBuffer() { } } -// 记录 hover 事件 +// Record hover event export function recordHoverEvent(action: "enter" | "leave", target: HTMLElement) { // const tracer = trace.getTracer('web-hover'); // tracer.startActiveSpan(`hover.${action}`, (span) => { @@ -460,7 +460,7 @@ export function recordHoverEvent(action: "enter" | "leave", target: HTMLElement) // }); } -// 记录拖拽事件 +// Record drag event export function recordDragEvent( action: "start" | "move" | "end", target: HTMLElement, @@ -491,7 +491,7 @@ export function recordDragEvent( }); } -// 记录键盘快捷键 +// Record keyboard shortcut export function recordKeyboardShortcut(key: string, modifiers: string[], target?: HTMLElement) { const tracer = trace.getTracer("web-keyboard"); tracer.startActiveSpan("keyboard.shortcut", (span) => { @@ -517,17 +517,17 @@ export function recordKeyboardShortcut(key: string, modifiers: string[], target? }); } -// 记录滚动事件 +// Record scroll event let scrollBuffer: Array<{ x: number; y: number; timestamp: number }> = []; let scrollTimer: NodeJS.Timeout | null = null; let lastScrollTime = 0; -const SCROLL_THROTTLE_MS = 100; // 每100ms最多记录一次 -const SCROLL_BATCH_SIZE = 5; // 每5个点或每500ms处理一次 +const SCROLL_THROTTLE_MS = 100; // Record at most once every 100ms +const SCROLL_BATCH_SIZE = 5; // Process every 5 points or every 500ms export function recordScrollEvent(x: number, y: number, target?: HTMLElement | Window) { const now = Date.now(); - // 节流:如果距离上次记录时间太短,直接返回 + // Throttle: if the time since the last record is too short, return directly if (now - lastScrollTime < SCROLL_THROTTLE_MS) { return; } @@ -535,13 +535,13 @@ export function recordScrollEvent(x: number, y: number, target?: HTMLElement | W lastScrollTime = now; scrollBuffer.push({ x, y, timestamp: now }); - // 如果缓冲区达到批量大小,立即处理 + // If the buffer reaches the batch size, process it immediately if (scrollBuffer.length >= SCROLL_BATCH_SIZE) { processScrollBuffer(target); return; } - // 设置定时器,如果500ms内没有达到批量大小,也会处理 + // Set a timer, if the batch size is not reached within 500ms, it will also be processed if (!scrollTimer) { scrollTimer = setTimeout(() => { processScrollBuffer(target); @@ -569,7 +569,7 @@ function processScrollBuffer(target?: HTMLElement | Window) { span.setAttribute("scroll.timestamp", new Date().toISOString()); span.setAttribute("page.url", window.location.href); - // 计算滚动距离 + // Calculate scroll distance const deltaX = scrollBuffer[scrollBuffer.length - 1].x - scrollBuffer[0].x; const deltaY = scrollBuffer[scrollBuffer.length - 1].y - scrollBuffer[0].y; const totalDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); @@ -578,11 +578,11 @@ function processScrollBuffer(target?: HTMLElement | Window) { span.setAttribute("scroll.delta_y", deltaY); span.setAttribute("scroll.total_distance", totalDistance); - // 滚动方向 + // Scroll direction const direction = deltaY > 0 ? "down" : deltaY < 0 ? "up" : "none"; span.setAttribute("scroll.direction", direction); - // 目标信息 + // Target information if (target && target !== window) { const element = target as HTMLElement; span.setAttribute("scroll.target.tag_name", element.tagName); @@ -592,7 +592,7 @@ function processScrollBuffer(target?: HTMLElement | Window) { span.setAttribute("scroll.target", "window"); } - // 视口信息 + // Viewport information span.setAttribute("viewport.width", window.innerWidth); span.setAttribute("viewport.height", window.innerHeight); span.setAttribute("document.scroll_width", document.documentElement.scrollWidth); @@ -609,7 +609,7 @@ function processScrollBuffer(target?: HTMLElement | Window) { } }); - // 清理缓冲区 + // Clear buffer scrollBuffer = []; if (scrollTimer) { clearTimeout(scrollTimer); @@ -617,7 +617,7 @@ function processScrollBuffer(target?: HTMLElement | Window) { } } -// 记录表单取消 +// Record form cancellation export function recordFormCancel(form: HTMLFormElement, reason?: string) { const tracer = trace.getTracer("web-form"); tracer.startActiveSpan("form.cancel", (span) => { diff --git a/src/event-listeners.ts b/src/event-listeners.ts index a6eb18f..1a471c8 100644 --- a/src/event-listeners.ts +++ b/src/event-listeners.ts @@ -1,4 +1,4 @@ -// event-listeners.ts - 自动事件监听器 +// event-listeners.ts - Automatic event listeners import { trace } from "@opentelemetry/api"; import { recordDragEvent, @@ -18,10 +18,10 @@ let isGlobalScrollRecordingEnabled = false; let globalScrollEventHandler: ((event: Event) => void) | null = null; type Configs = { - /** 是否开启滚动监听, 默认不开启 */ + /** Whether to enable scroll listening, disabled by default */ observeScroll?: boolean; }; -// 初始化所有事件监听器 +// Initialize all event listeners export function initializeEventListeners({ observeScroll }: Configs) { if (typeof window === "undefined") { console.log("⚠️ Skipping event listeners initialization on server side"); @@ -30,18 +30,18 @@ export function initializeEventListeners({ observeScroll }: Configs) { console.log("🎧 Initializing event listeners..."); - // 配置是否开启滚动监听 + // Configure whether to enable scroll listening disableGlobalScrollRecording(); if (observeScroll === true) { enableGlobalScrollRecording(); } - // 页面卸载监听 + // Page unload listener window.addEventListener("beforeunload", () => { recordPageUnload(); }); - // 页面缩放监听 + // Page zoom listener let lastZoom = window.devicePixelRatio; const zoomObserver = new ResizeObserver(() => { const currentZoom = window.devicePixelRatio; @@ -52,7 +52,7 @@ export function initializeEventListeners({ observeScroll }: Configs) { }); zoomObserver.observe(document.body); - // 滚动事件监听 + // Scroll event listener globalScrollEventHandler = () => { // Only record if global scroll recording is enabled if (isGlobalScrollRecordingEnabled) { @@ -64,12 +64,12 @@ export function initializeEventListeners({ observeScroll }: Configs) { }); console.log("📜 Global scroll event listener initialized"); - // 鼠标移动监听(节流) + // Mouse move listener (throttled) document.addEventListener("mousemove", (event) => { recordMouseMove(event.clientX, event.clientY); }); - // Hover 事件监听 + // Hover event listener document.addEventListener( "mouseenter", (event) => { @@ -90,7 +90,7 @@ export function initializeEventListeners({ observeScroll }: Configs) { true, ); - // 拖拽事件监听 + // Drag event listener document.addEventListener("dragstart", (event) => { if (event.target instanceof HTMLElement) { recordDragEvent("start", event.target, { @@ -114,7 +114,7 @@ export function initializeEventListeners({ observeScroll }: Configs) { } }); - // 键盘快捷键监听 + // Keyboard shortcut listener document.addEventListener("keydown", (event) => { const modifiers: string[] = []; if (event.ctrlKey) modifiers.push("Ctrl"); @@ -122,30 +122,30 @@ export function initializeEventListeners({ observeScroll }: Configs) { if (event.altKey) modifiers.push("Alt"); if (event.shiftKey) modifiers.push("Shift"); - // 只记录有修饰键的快捷键 + // Only record shortcuts with modifier keys if (modifiers.length > 0) { recordKeyboardShortcut(event.key, modifiers, event.target as HTMLElement); } }); - // 表单取消监听 + // Form cancellation listener document.addEventListener("reset", (event) => { if (event.target instanceof HTMLFormElement) { recordFormCancel(event.target, "reset"); } }); - // SPA 路由变化监听(History API) + // SPA route change listener (History API) let currentUrl = window.location.href; - // 监听 popstate 事件(浏览器前进/后退) + // Listen for popstate events (browser back/forward) window.addEventListener("popstate", () => { const newUrl = window.location.href; recordRouteChange(currentUrl, newUrl, "back"); currentUrl = newUrl; }); - // 重写 pushState 和 replaceState 方法 + // Override pushState and replaceState methods const originalPushState = history.pushState; const originalReplaceState = history.replaceState; @@ -163,7 +163,7 @@ export function initializeEventListeners({ observeScroll }: Configs) { currentUrl = newUrl; }; - // 双击和右键监听 + // Double-click and right-click listeners document.addEventListener("dblclick", (event) => { if (event.target instanceof HTMLElement) { const tracer = trace.getTracer("web-interaction"); diff --git a/src/recordChange.ts b/src/recordChange.ts index 58a0285..3d2e84f 100644 --- a/src/recordChange.ts +++ b/src/recordChange.ts @@ -1,7 +1,7 @@ import { trace } from "@opentelemetry/api"; import { getElementXPath } from "./getElementXPath"; -// 处理select标签的change事件 +// Handle change events for select elements const handleSelectChange = (target: HTMLSelectElement) => { const spanType = "select_change"; const tracer = trace.getTracer("web-interaction"); @@ -25,7 +25,7 @@ const handleSelectChange = (target: HTMLSelectElement) => { }); }; -// 处理input[type='date']标签的change事件 +// Handle change events for input[type='date'] elements const handleInputDateChange = (target: HTMLInputElement) => { const spanType = "input_date_change"; const tracer = trace.getTracer("web-interaction");