模式一:直接扫码(直连上报)⭐ 推荐

适用场景

  • App 内部已经封装了 HTTP 请求拦截/日志记录能力
  • 不想安装 CA 证书
  • 想最小化改动

工作原理

┌────────┐                        ┌────────────┐                ┌────────┐
│  App   │ ──拦截 HTTP 请求─────> │ App 拦截器  │ ──POST 上报──> │ 服务器  │
│        │ <──正常返回结果──────  │ (业务代码)  │ <──200 OK───   │ 443    │
└────────┘                        └────────────┘                └────────┘

App 在自己封装的 HTTP 请求层(OkHttp/AFNetworking/axios 等)拦截到请求和响应后,主动调用上报接口推送到沙特协作服务器。

App 端接入步骤

步骤 1:扫码获取接入信息

桌面端启动后,左侧入口码区域显示二维码。手机 App 扫描后自动获取:

{
  "shater_url": "https://shater.online/shater/raw-data?sessionId=7a58592c72f04fe",
  "shater_type": "web",
  "sessionId": "7a58592c72f04fe"
}

步骤 2:在 App 公共请求方法中上报

Android(OkHttp 示例)

// 全局 OkHttp 拦截器
class ShaterCollabInterceptor(private val sessionId: String, private val reportUrl: String) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val startTime = System.currentTimeMillis()

        // 执行原请求
        val response = chain.proceed(request)
        val endTime = System.currentTimeMillis()
        val timeConsuming = endTime - startTime

        // 异步上报(不阻塞业务)
        val body = response.peekBody(1024 * 1024) // 最多 1MB
        Thread {
            try {
                val reportBody = FormBody.Builder()
                    .add("api", request.url.encodedPath)
                    .add("method", request.method)
                    .add("requestHeader", request.headers.toString())
                    .add("request", bodyToString(request.body))
                    .add("responseHeader", response.headers.toString())
                    .add("response", body.string())
                    .add("timeConsuming", timeConsuming.toString())
                    .build()

                val report = Request.Builder()
                    .url("$reportUrl?sessionId=$sessionId")
                    .post(reportBody)
                    .build()

                OkHttpClient().newCall(report).execute().close()
            } catch (e: Exception) {
                // 上报失败不影响业务
            }
        }.start()

        return response
    }
}

iOS(AFNetworking/URLSession 示例)

// URLSession 拦截示例
- (void)URLSession:(NSURLSession *)session
              dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveResponse:(NSURLResponse *)response
    completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {

    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
    NSMutableURLRequest *request = dataTask.originalRequest;
    NSTimeInterval startTime = [self.startTimes[@(dataTask.taskIdentifier)] doubleValue];
    NSTimeInterval timeConsuming = ([[NSDate date] timeIntervalSince1970] * 1000) - startTime;

    // 异步上报
    NSString *reportUrl = [NSString stringWithFormat:@"%@/shater/raw-data?sessionId=%@", self.serverUrl, self.sessionId];
    NSMutableURLRequest *reportReq = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:reportUrl]];
    reportReq.HTTPMethod = @"POST";
    [reportReq setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];

    NSString *body = [NSString stringWithFormat:@"api=%@&method=%@&requestHeader=%@&request=%@&responseHeader=%@&response=%@&timeConsuming=%f",
                      [self urlEncode:request.URL.path],
                      request.HTTPMethod,
                      [self urlEncode:request.allHTTPHeaderFields.description],
                      @"", // 请求体
                      [self urlEncode:httpResponse.allHeaderFields.description],
                      @"", // 响应体
                      timeConsuming];
    reportReq.HTTPBody = [body dataUsingEncoding:NSUTF8StringEncoding];

    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:reportReq];
    [task resume];

    completionHandler(NSURLSessionResponseAllow);
}

Web/JavaScript(fetch 示例)

// 全局 fetch 拦截
const originalFetch = window.fetch;
window.fetch = async function(input, init) {
  const startTime = Date.now();
  const response = await originalFetch(input, init);
  const timeConsuming = Date.now() - startTime;

  // 异步上报
  fetch(`${REPORT_URL}?sessionId=${SESSION_ID}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      api: new URL(input, location.href).pathname,
      method: init?.method || 'GET',
      requestHeader: JSON.stringify(init?.headers || {}),
      request: init?.body ? String(init.body) : '',
      responseHeader: JSON.stringify(Object.fromEntries(response.headers.entries())),
      response: await response.clone().text(),
      timeConsuming,
    }),
  }).catch(() => {});

  return response;
};

步骤 3:服务端接收上报

上报接口:POST /shater/raw-data?sessionId=<sessionId>

详细字段说明见 推送拦截数据 章节。

优势

  • ✅ 无需安装 CA 证书
  • ✅ 不修改请求地址
  • ✅ HTTPS 明文可见(因为 App 自己能解密)
  • ✅ 不影响业务性能(异步上报)

限制

  • ❌ 需要 App 端配合(在拦截器中主动调用)
  • ❌ 请求体/响应体需要在内存中可读(peekBody / clone()