Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/**"],
};
44 changes: 44 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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`).
66 changes: 49 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -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",
// 上报数据地址: <INSPECTOR_COLLECTOR_URL>/v1/traces
apiKey: "",
userId: "",
serviceName: "YOUR_SERVICE_NAME",
// Data collector endpoint: <INSPECTOR_COLLECTOR_URL>/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();
}
};
Expand Down
6 changes: 3 additions & 3 deletions src/CustomConsoleSpanExporter.ts
Original file line number Diff line number Diff line change
@@ -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`);
Expand All @@ -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));
}
Expand Down
14 changes: 7 additions & 7 deletions src/SessionManager.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 "";

Expand All @@ -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 "";

Expand All @@ -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");
Expand Down
36 changes: 18 additions & 18 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand All @@ -41,22 +41,22 @@ export function initBrowserInspector(config: InspectorConfig): Promise<{
);
}

// 构造resource
// Construct resource
const resource = createUserResource({
apiKey: config.apiKey,
userId: config.userId,
serviceName: config.serviceName,
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({
Expand All @@ -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",
Expand All @@ -89,20 +89,20 @@ 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");
} else {
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"
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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 }]) => {
Expand All @@ -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();
}
}
Expand Down
10 changes: 5 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
export type InspectorConfig = {
// 业务相关
// Business related
apiKey: string;
userId: string;
/** 区分业务线 */
/** Differentiate business lines */
serviceName: string;
/** 接收span的地址,POST <collectorEndpoint>/v1/traces */
/** Address for receiving spans, POST <collectorEndpoint>/v1/traces */
collectorEndpoint?: string;
/** 'dev' | 'prd' */
env?: string;

// 配置相关
/** 是否开启滚动监听, 默认不开启 */
// Configuration related
/** Whether to enable scroll listening, disabled by default */
observeScroll?: boolean;
};
Loading