国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

SourceMap 與前端異常監(jiān)控

共 31892字,需瀏覽 64分鐘

 ·

2021-07-01 22:38

?? 加個關(guān)注,后續(xù)上新不錯過~

背景

我們從事 Web 開發(fā)工作中,異常監(jiān)控系統(tǒng)已經(jīng)是我們朝夕相處的好助手,但是這些異常處理工具通常都是建立在 Web 生態(tài),或者是假定運行在瀏覽器環(huán)境下的,但是當我們需要給一套跨端系統(tǒng)搭建一套類似的異常監(jiān)控系統(tǒng),并且期望該系統(tǒng)兼容 Web 生態(tài),現(xiàn)有的工具很可能就不滿足我們的需求了,因此我們需要考慮一套完整的異常監(jiān)控系統(tǒng)整個鏈路將會涉及到哪些工具鏈,以及如何修改這些工具鏈來適配我們的跨端系統(tǒng)。

精彩的一天從查 Bug 開始

我們先從和我們程序員最息息相關(guān)的線上查 Bug 開始。

下面這種圖,相信大家很多人都很熟悉,當我們收到線上 Bug 反饋或者收到報警電話時,第一時間基本就是去自己的監(jiān)控平臺去查看線上日志,大家很可能看到類似下面這張截圖


有經(jīng)驗的老司機,立馬就可以定位到自己代碼里哪里出了問題,但是有沒有仔細思考過整套監(jiān)控系統(tǒng)是如何打通的呢?或者說如果有一天你的監(jiān)控系統(tǒng)出了問題,你知道如何追查是哪個環(huán)節(jié)出了問題嗎?

代碼反解

有經(jīng)驗的老司機立馬就反應(yīng)過來了,不就是代碼里集成下 Sentry 的 Client,然后每次把 SourceMap 也上傳一份給 Sentry,線上遇到錯誤將錯誤上傳給 Sentry Server,Sentry Server基于錯誤堆棧和 SourceMap 反解出原始的堆棧就可以了。是的,監(jiān)控系統(tǒng)要解決的一個核心問題就是代碼反解。

出于一些性能和安全等的考慮,通常我們發(fā)布到線上的代碼,通常并非原始的代碼,而是經(jīng)過混淆壓縮后的代碼,即使不經(jīng)過壓縮,大部分的前端工程都會經(jīng)過一個 build 的過程,這個過程里通常會包括代碼的轉(zhuǎn)換、打包和壓縮等,這使得調(diào)試生成的代碼變得異常困難,因此,我們需要一個工具幫我們解決這類調(diào)試問題。

SourceMap

SourceMap 幾乎完美的解決了代碼反解問題,其使用方式十分簡單,我們在編譯的時候除了生成最終產(chǎn)物 xxx.js 文件外還會額外生成一個 xxx.js.map 的文件,這個 map 文件里包含了原始代碼及其位置映射信息,這樣我們利用 xxx.js 和 xxx.js.map 就可以將 xxx.js 的代碼及其位置完美的映射會源代碼以及位置,這樣我們的調(diào)試工具就可以基于這個 map 文件實現(xiàn)源碼調(diào)試了。其原理雖然很簡單,但是當我們在工程中實際應(yīng)用 SourceMap 的時候,仍然會碰到這樣或那樣的問題。一個很常見的問題就是,為啥用戶上報的錯誤沒法反解為原始代碼的錯誤堆棧了?

SourceMap 支持的全鏈路流程

SourceMap 的使用并非是簡單的一個編譯生成即可,其實際上是需要我們整個的工作鏈路進行配合,才能使得 SourceMap 可以正常工作,因此我們需要先看看我們的整個工作鏈路上哪些環(huán)節(jié)會涉及 SourceMap,以及可能會碰到哪些問題。

以一個業(yè)務(wù)場景為例,我們用 Vue 開發(fā)的應(yīng)用部署到線上 -> 發(fā)生了異常 -> 上報到了 Sentry -> Sentry 幫我們將錯誤進行反解展示給我們。這個業(yè)務(wù)場景非常簡單但是實際涉及到了很多 SourceMap 的處理。

Transformer

首先我們需要我們的 DSL Transformer 支持 SourceMap

// App.vue
<template>
  <div></div>
</template>
<script lang="ts">
  let x: 10;
export default {}
</script>
<style>
h1 {
  color: red;
}
</style>
<i18n>
{ "greeting": "hello" }
</i18n>

我們使用 @vue/compiler-sfc 將該 Vue 文件編譯為一個 SFCRecord,此時 SFCRecord 里實際上包含了每個 block 的 SourceMap

const { parse } = require('@vue/compiler-sfc');
const fs = require('fs');
const path = require('path');

async function main(){
  const content = fs.readFileSync(path.join(__dirname, './App.vue'),'utf-8');
  const sfcRecord = parse(content);
  const map = sfcRecord.descriptor['styles'][0].map
  console.log('sfc:',map) // 打印style的SourceMap
}

main();

我們可以進一步的根據(jù)每個 block 的 tag 和 lang 來繼續(xù) transform 每個 block,如使用 Babel | Typescript 處理 Script,PostCSS來處理 Style,Pug 來處理 Template,這里每個 Transformer 也都需要處理 SourceMap。

Bundler

處理完 Vue 文件的編譯后,我們希望通過一個 bundler 來處理 Vue 模塊的打包,此時我們可以使用esbuild、rollup、或者 Webpack,我們這里使用 rollup-plugin-vue 來配合 rollup 給 Vue 應(yīng)用進行打包。

async function bundle(){
  const bundle = await rollup.rollup({
    input: [path.join(__dirname, './App.vue')],
    plugins:[rollupPluginVue({needMap:true})],
    external: ['vue'],
    output: {
      sourcemap:'inline'
    }
   
  })
  const result = await bundle.write({
    output: {
      file'bundle.js',
      sourcemap:true
    },
  })
  for(const chunk of result.output){
    console.log('chunk:',chunk.map) // SourceMap
  }
  
}
bundle()

因此這里也需要 bundle 在進行 bundler 的過程中同時處理生成 sourcemap。

Minifier

但我們 bundler 完代碼后,還需要將代碼進行壓縮混淆才能發(fā)布到線上,這時我們需要使用 minify 工具進行混淆壓縮。我們使用 terser 進行壓縮。壓縮時不僅需要處理 minfy 過程生成的 SourceMap 還需要處理其和原始 bundler 生成的 SourceMap 合并的問題,否則 SourceMap 和經(jīng)過壓縮處理的代碼對應(yīng)不上了。

  for(const chunk of result.output){
    console.log('chunk:', chunk.map)
    const minifyResult = await require('terser').minify(chunk.code, {
      sourceMap:true,
    })
    console.log('minifyMap:', minifyResult.map)
  }

Runtime

經(jīng)過一番折騰,我們的編譯流程終于處理完 SourceMap 了,我們開發(fā)過程中突然發(fā)現(xiàn)了代碼出問題了,我們希望錯誤的堆棧能顯示源碼的位置,另外能支持源碼調(diào)試應(yīng)用,這時候就需要用的瀏覽器的 SourceMap 支持和 node 的 SourceMap 支持了。

日志收集和上報

經(jīng)過一番眼花繚亂的操作,我們的代碼終于和 SourceMap 對應(yīng)上了,我們平穩(wěn)的將業(yè)務(wù)部署上線,上線前我們需要確保我們的錯誤能夠以正確的格式上報到我們的日志平臺,然而我們線上運行的平臺那么多樣,運行的 JS 引擎也是各式各樣,我們需要將用戶的錯誤統(tǒng)一成一個格式上報給平臺,幸運的是 Sentry 的客戶端已經(jīng)幫我們做了這件事情。我們只需要考慮接入 Sentry 的客戶端就行了。因為如果直接將 SourceMap 一起跟隨 js 代碼下發(fā),這就導(dǎo)致用戶可以直接窺探你的源碼了,類似發(fā)生這樣的事情就很尷尬了https://zhuanlan.zhihu.com/p/26033573,因此我們還需要考慮將 SourceMap 發(fā)布到內(nèi)網(wǎng)而非公網(wǎng)上,這時就需要處理 SourceMap 關(guān)聯(lián)的問題了。

錯誤日志反解

一切都妥當了,只需要等用戶的錯誤上報上來(最好永遠別來),我就可以在 Sentry 上查看用戶的原始錯誤堆棧,幫用戶排查問題了,這時候?qū)嶋H上 Sentry Server 端偷偷幫我們做了根據(jù)用戶的錯誤棧和用戶的 SourceMap,幫我們反解錯誤棧的事情了。

總結(jié)一下,一個完整的 SourceMap 流程支持包括了如下這些步驟:

  • transformer: Babel、typescript、emscripten 、 esbuild
  • minifier: esbuild ,terser
  • bundler: esbuild, webpack, rollup
  • runtime: browser & node &  deno
  • 日志上報: sentry client
  • 錯誤日志反解: sentry server && node-sourcemap-support

上面這些流程,基本上大多數(shù)工具都幫我們封裝好了,我們只需要安心使用即可,但是當某天你需要自己開發(fā)一個自定義的 DSL 的 transformer 通過自研的 bundler 進行編譯打包,運行在自研的 JS 引擎上并且使用自研的 monitor client 上報到自研的 apm 平臺上,任何環(huán)節(jié)的出錯都可能導(dǎo)致你線上的錯誤日志反解前功盡棄,你所能做的就是在整個鏈路上進行分析定位。

我們接下來就看看整個鏈路上有多少種出錯的風險和可能,并且如何定位修復(fù)這些問題。

SourceMap 格式

首先我們需要了解下 SourceMap 的基本格式

我們將一個 .ts 文件編譯為 .js 文件,看看其 SourceMap 信息是如何處理映射的。我們項目包含了原始的 ts 文件 add.ts、編譯后的產(chǎn)物文件 add.js 和 SourceMap 文件 add.js.map,其內(nèi)容如下

  • add.ts
const add = (x:number, y:number) => {
  return x + y;
}
  • add.js
var add = function (x, y{
    return x + y;
};
//# sourceMappingURL=module.js.map

SourceMap 的規(guī)范本身十分精簡和清晰,其本身是一個 JSON 文件,包含如下幾個核心字段

 {
   version : 3// SourceMap標準版本,最新的為3
   file: "add.js"// 轉(zhuǎn)換后的文件名
   sourceRoot : ""// 轉(zhuǎn)換前的文件所在目錄,如果與轉(zhuǎn)換前的文件在同一目錄,該項為空
   sources: ["add.ts"], // 轉(zhuǎn)換前的文件,該項是一個數(shù)組,表示可能存在多個文件合并
   names: [], // 轉(zhuǎn)換前的所有變量名和屬性名,多用于minify的場景
   sourcesContent: [ // 原始文件內(nèi)容
    "const add = (x:number,y:number) => {\n  return x+y;\n}"
  ]
  mappings: "AAAA,IAAM,GAAG,GAAG,UAAC,CAAQ,EAAC,CAAQ;IAC5B,OAAO,CAAC,GAAC,CAAC,CAAC;AACb,CAAC,CAAA",
 }

簡單介紹下 mapping 的格式,mapping 實際上是個三級結(jié)構(gòu),我們以上述的例子為例

  • line:每個 mapping 包含由 ; 分割的多行內(nèi)容
lines = mappings.split(';')
//  [
  'AAAA,IAAM,GAAG,GAAG,UAAC,CAAQ,EAAC,CAAQ'// var add = function (x, y) {
  'IAC5B,OAAO,CAAC,GAAC,CAAC,CAAC',     // return x + y;
  'AACb,CAAC,CAAA' // };
]

其中每一行都對應(yīng)生成代碼的每行文件的位置映射信息,這里的三行分別對應(yīng)了編譯產(chǎn)物的三行信息

  • segment:每一行同包含由 , 分割的多個 segment 信息,其中每個 segment 都對應(yīng)了產(chǎn)物里每一行里每一個符合所在的列的信息
const segments = lines.map(x => {
  return x.split(',')
})

console.log('segments:',segments)
//  [
  [
    'AAAA''IAAM',
    'GAAG''GAAG',
    'UAAC''CAAQ',
    'EAAC''CAAQ'
  ], 
  [ 'IAC5B''OAAO''CAAC''GAAC''CAAC''CAAC' ],
  [ 'AACb''CAAC''CAAA' ]
]
  • fields:每個 segment 實際上又包含了幾個 field,每個 field 都編碼了具體的行列映射信息,依次為
    • 第一位: 轉(zhuǎn)換后代碼所處的列號,如果這是當前行的第一個 segment,那么是個絕對值,否則是相對于上一個 segment 的相對值
    • 第二位:表示這個位置屬于 sources 屬性中的哪一個文件,相對于前一個 segment 的位置(區(qū)別于列號,下一行的第一個 segment 仍然是相對于上一行的最后一個 segment,并不會 reset)
    • 第三位:表示這個位置屬于轉(zhuǎn)換前代碼的第幾行,相對位置,同第二列
    • 第四位:表示這個位置屬于轉(zhuǎn)換前代碼的第幾列,相對位置,同第二列
    • 第五位:表示這個位置屬于 names 屬性中的哪一個變量,相對位置,同第二列

這里 field 存儲的值并非是直接的數(shù)字值,而是將數(shù)字使用 vlq 進行了編碼,根據(jù)上述這些信息我們實際上就可以實現(xiàn) SourceMap 的雙向映射了,即可以根據(jù) SourceMap 和原始代碼的位置信息查找到生成代碼的信息,也可以根據(jù) SourceMap 和生成代碼的位置信息,查找到原始代碼的信息。接下來我們就實踐下如何進行代碼位置的雙向查找。

雙向查找流程

vlq 解碼

首先第一步我們需要將 vlq 編碼的 SourceMap 反解為原始的數(shù)字偏移信息,我們可以直接使用封裝好的 vlq 庫完成這一步

function decode({
  const { decode} = require('vlq')
  const mappings = JSON.parse(result.sourceMapText).mappings;
  console.log('mappings:', mappings)
  /**
   * @type {string[]}
   */

  const lines = mappings.split(';');
  const decodeLines = lines.map(line => {
    const segments = line.split(',');
    const decodedSeg = segments.map(x => {
      return decode(x)
    })
    return decodedSeg;
  })
  console.log(decodeLines)
}

此時我們得到一個解碼后的位置信息

[
  [
    [ 0000 ],
    [ 4006 ],
    [ 3003 ],
    [ 3003 ],
    [ 10001 ],
    [ 1008 ],
    [ 2001 ],
    [ 1008 ]
  ],
  [
    [ 401-28 ],
    [ 7007 ],
    [ 1001 ],
    [ 3001 ],
    [ 1001 ],
    [ 1001 ]
  ],
  [ [ 001-13 ], [ 1001 ], [ 1000 ] ]
]

還原絕對位置索引

此時的這些位置信息都是相對位置,我們需要將其還原為絕對位置

  const decoded = decodeLines.map((line) => {
    absSegment[0] = 0// 每行的第一個segment的位置要重置
    if (line.length == 0) {
      return [];
    }
    const absoluteSegment = line.map((segment) => {
      const result = [];
      for (let i = 0; i < segment.length; i++) {
        absSegment[i] += segment[i];
        result.push(absSegment[i]);
      }
      return result;
    });
    return absoluteSegment;
  });
  console.log('decoded:', decoded)
}

結(jié)果如下,此時為絕對位置映射表

 [
  [
    [ 0000 ],
    [ 4006 ],
    [ 7009 ],
    [ 100012 ],
    [ 200013 ],
    [ 210021 ],
    [ 230022 ],
    [ 240030 ]
  ],
  [
    [ 4012 ],
    [ 11019 ],
    [ 120110 ],
    [ 150111 ],
    [ 160112 ],
    [ 170113 ]
  ],
  [ [ 0020 ], [ 1021 ], [ 2021 ] ]
]

雙向映射

有了這個絕對位置映射,我們就可以構(gòu)建源碼和產(chǎn)物的雙向映射了。我們可以實現(xiàn)兩個核心 API

originalPositionFor 用于根據(jù)產(chǎn)物的行列號,查找對應(yīng)源碼的信息,而generatedPositionFor 則是根據(jù)源碼的文件名、行列號,查找產(chǎn)物里的位置信息。

class SourceMap {
  constructor(rawMap) {
    this.decode(rawMap);
    this.rawMap = rawMap
  }
 
  /**
   * 
   * @param {number} line 
   * @param {number} column 
   */

  originalPositionFor(line, column){
    const lineInfo = this.decoded[line];
    if(!lineInfo){
      throw new Error(`不存在該行信息:${line}`);
    }
    const columnInfo = lineInfo[column];
    for(const seg of lineInfo){
      // 列號匹配
      if(seg[0] === column){
        const [column, sourceIdx,origLine, origColumn] = seg;
        const source = this.rawMap.sources[sourceIdx]
        const sourceContent = this.rawMap.sourcesContent[sourceIdx];
        const result = codeFrameColumns(sourceContent, {
         start: {
           line: origLine+1,
           column: origColumn+1
         }
        }, {forceColor:true})
        return {
          source,
          line: origLine,
          column: origColumn,
          frame: result
        }
      }
    }
    throw new Error(`不存在該行列號信息:${line},${column}`)
  }
  
  decode(rawMap) {
    const {mappings} = rawMap
    const { decode } = require('vlq');
    console.log('mappings:', mappings);
    /**
     * @type {string[]}
     */

    const lines = mappings.split(';');
    const decodeLines = lines.map((line) => {
      const segments = line.split(',');
      const decodedSeg = segments.map((x) => {
        return decode(x);
      });
      return decodedSeg;
    });
    const absSegment = [00000];
    const decoded = decodeLines.map((line) => {
      absSegment[0] = 0// 每行的第一個segment的位置要重置
      if (line.length == 0) {
        return [];
      }
      const absoluteSegment = line.map((segment) => {
        const result = [];
        for (let i = 0; i < segment.length; i++) {
          absSegment[i] += segment[i];
          result.push(absSegment[i]);
        }
        return result;
      });
      return absoluteSegment;
    });
    this.decoded = decoded;
  }
}

const consumer = new SourceMap(rawMap);

console.log(consumer.originalPositionFor(0,21).frame)

我們還可以使用 codeFrame 直接可視化查找出源碼的上下文信息

generatedPositionFor 的實現(xiàn)原理類似,不再贅述。

事實上上面這些反解流程并不需要我們自己去實現(xiàn),https://github.com/mozilla/source-map 已經(jīng)幫我們提供了很多的編譯方法,包括不限于

  • originalPositionFor:查找源碼位置

  • generatedPositionFor:查找生成代碼位置

  • eachMapping:生成每個 segment 的詳細映射信息

Mapping {
  generatedLine: 2,
  generatedColumn: 17,
  lastGeneratedColumn: null,
  source: 'add.ts',
  originalLine: 2,
  originalColumn: 13,
  name: null
}
Mapping {
  generatedLine: 3,
  generatedColumn: 0,
  lastGeneratedColumn: null,
  source: 'add.ts',
  originalLine: 3,
  originalColumn: 0,
  name: null
}
Mapping {
  generatedLine: 3,
  generatedColumn: 1,
  lastGeneratedColumn: null,
  source: 'add.ts',
  originalLine: 3,
  originalColumn: 1,
  name: null
}
Mapping {
  generatedLine: 3,
  generatedColumn: 2,
  lastGeneratedColumn: null,
  source: 'add.ts',
  originalLine: 3,
  originalColumn: 1,
  name: null
}

事實上 Sentry 的 SourceMap 反解功能也是基于此實現(xiàn)的。

SourceMap 全鏈路支持

前面我們已經(jīng)介紹的 SourceMap 的基本格式,以及如何基于 SourceMap 的內(nèi)容,來實現(xiàn) SourceMap 的雙向查找功能,大部分的 sourcmap 相應(yīng)的工具鏈都是基于此設(shè)計的,但是在給整個鏈路做 SourceMap 支持的時候,但是鏈路的每一步需要解決的問題卻各有不同(的坑),我們來一步步的研(踩)究(坑)吧。

給 transformer 添加 SourceMap 映射

Web 社區(qū)的主流語言的工具鏈都已經(jīng)有了內(nèi)置的 SourceMap 支持了,但是如果你自行設(shè)計一個 DSL 要怎么給其添加 SourceMap 支持呢?事實上 SourceMapGenerator 給我們提供了便捷的生成 SourceMap 內(nèi)容的方法,但是當我們處理各種字符串變換的時候,直接使用其 API 仍然較為繁瑣。幸運的是很多工具封裝了生成 SourceMap 的操作,提供了較為上層的 api。我們自己實現(xiàn) transformer 主要分為兩種場景,一種是基于 AST 的變換,另一種則是對字符串(可能壓根不存在 AST)的增刪改查。

  • AST 變換

大部分的前端 transform 工具,都內(nèi)置幫我們處理好了 SourceMap 的映射,我們只需要關(guān)心如何處理 AST 即可,以 babel 為例,并不需要我們手動的進行 SourceMap 節(jié)點的操作

import babel from '@babel/core';
import fs from 'fs';
const result = babel.transform('a === b;', {
  sourceMapstrue,
  filename'transform.js',
  plugins: [
    {
      name'my-plugin',
      pre() => {
        console.log('xx');
      },
      visitor: {
        BinaryExpression(path, t) {
          let tmp = path.node.left;
          path.node.left = path.node.right;
          path.node.right = tmp;
        }
      }
    }
  ]
});
console.log(result.code, result.map);
// 結(jié)果
b === a; 
{
  version3,
  sources: [ 'transform.js' ],
  names: [ 'b''a' ],
  mappings'AAAMA,CAAN,KAAAC,CAAC',
  sourcesContent: [ 'a === b;' ]
}
  • 但是 AST 并不能覆蓋所有場景,例如我們?nèi)绻枰獙?c++ 或者 brainfuck 編譯為 js,就很難找到便捷的工具,或者我們只需要替換代碼里的部分內(nèi)容,AST 分析就是大才小用了。此時我們可以使用 magic-string 來實現(xiàn)。
const MagicString = require('magic-string');
const s = new MagicString('problems = 99');

s.overwrite(08'answer');
s.toString(); // 'answer = 99'

s.overwrite(1113'42'); // character indices always refer to the original string
s.toString(); // 'answer = 42'

s.prepend('var ').append(';'); // most methods are chainable
s.toString(); // 'var answer = 42;'

const map = s.generateMap({
  source'source.js',
  file'converted.js.map',
  includeContenttrue
}); // generates a v3 SourceMap

console.log('code:', s.toString());
console.log('map:', map);
// 結(jié)果 
code: var answer = 42;
map: SourceMap {
  version3,
  file'converted.js.map',
  sources: [ 'source.js' ],
  sourcesContent: [ 'problems = 99' ],
  names: [],
  mappings'IAAA,MAAQ,GAAG'
}

我們發(fā)現(xiàn)對于簡單的字符串處理,magic-string 比使用 AST 的方式要方便和高效很多。

SourceMap 驗證

當我們給我們的 transformer 加了 SourceMap 支持后,我們怎么驗證我們的 SourceMap 是正確的呢?你除了可以使用上面提到的 SourceMap 庫的雙向反解功能進行驗證外,一個可視化的驗證工具將大大簡化我們的工作。esbuild 作者就開發(fā)了一個 SourceMap 可視化驗證的網(wǎng)站 https://evanw.github.io/source-map-visualization/ 來幫我們簡化 SourceMap 的驗證工作。


SourceMap 合并

當我們處理好 transformer 的 SourceMap 生成之后,接下來就需要將 transformer 接入到 bundler 了,一定意義上 bundler 也可以視為一種 transformer,只是此時其輸入不再是單個源文件而是多個源文件。但這里牽扯到的一個問題是將 A 進行編譯生成了 B with SourceMap1  接著又將 B 進一步進行編譯生成了 C with SourceMap2,那么我們?nèi)绾胃鶕?jù) C 反解到 A 呢?很明顯使用 SourceMap2 只能幫助我們將 C 反解到 B,并不能反解到 A,大部分的反解工具也不支持自動級聯(lián)反解,因此當我們將 B 生成 C 的時候,還需要考慮將 SourceMap1 和 SourceMap2 進行合并,不幸的是很多 transformer 并不會自動的處理這種合并,如 TypeScript,但是大部分的 bundler 都是支持自動的 SourceMap 合并的。

如在 Rollup 里,你可以在 load 和 transform 里返回 code 的同時,返回 mapping。Rollup 會自動將該 mapping 和 builder 變換的 mapping 進行合并,vite 和 esbuild 也支持類似功能。如果我們需要自己處理 SourceMap 合并該如何操作,社區(qū)上已經(jīng)有庫幫我們處理這個事情。我們簡單看下

import ts from 'typescript';
import { minify } from 'terser';
import babel from '@babel/core';

import fs from 'fs';
import remapping from '@ampproject/remapping';
const code = `
const add = (a,b) => {
  return a+b;
}
`
;

const transformed = babel.transformSync(code, {
  filename'origin.js',
  sourceMapstrue,
  plugins: ['@babel/plugin-transform-arrow-functions']
});
console.log('transformed code:', transformed.code);
console.log('transformed map:', transformed.map);

const minified = await minify(
  {
    'transformed.js': transformed.code
  },
  {
    sourceMap: {
      includeSourcestrue
    }
  }
);
console.log('minified code:', minified.code);
console.log('minified map', minified.map);

const mergeMapping = remapping(minified.map, (file) => {
  if (file === 'transformed.js') {
    return transformed.map;
  } else {
    return null;
  }
});

fs.writeFileSync('remapping.js', minified.code);
fs.writeFileSync('remapping.js.map', minified.map);
//fs.writeFileSync('remapping.js.map', JSON.stringify(mergeMapping));

我們來簡單驗證下效果

  • 使用 mergeMapping 之前

  • 使用 mergeMapping 之后

我們可以看出做了 mergeSourcemap 后可以成功的還原出最初的源碼

性能 matters

我們支持好了上面的 SourceMap 生成和 SourceMap 合并了,迫不及待的在業(yè)務(wù)中加以使用了,但是卻“驚喜”的發(fā)現(xiàn)整個構(gòu)建流程的速度直線下降,因為 SourceMap 操作的開銷實際上是非常可觀的,在不需要 SourceMap 的情況下或者在對性能極其敏感的場景下(服務(wù)端構(gòu)建),實際是不建議默認開啟 SourceMap 的,事實上 SourceMap 對性能極其敏感,以至于 source-map 庫的作者們重新用 rust 實現(xiàn)了 source-map,并將其編譯到了 webassembly。

錯誤日志上報和反解

當我們處理好 SourceMap 的生成后,就可以進行日志上報了

Sentry

錯誤上報需要解決的一個問題就是統(tǒng)一上報格式問題,我們生產(chǎn)環(huán)境遇到的錯誤并非直接將原始的 Error 信息上報給服務(wù)端的,而是需要先進行格式化處理,如下面這種錯誤

function inner({
  myUndefinedFunction();
}
function outer({
  inner();
}
setTimeout(() => {
  outer();
}, 1000);

原始的錯誤堆棧如下


Sentry Client 會將其先進行格式化處理,Sentry 發(fā)送給后端的錯誤堆棧格式下面這種格式化數(shù)據(jù)


問題來了,為啥 Sentry 要經(jīng)過這樣一番格式化處理,以及格式化處理中可能會發(fā)生什么問題呢。

V8 StackTrace API

按理來講 Error 對象作為標準里規(guī)定的 Ordinary Object ,其在不同的 JS 引擎上表現(xiàn)行為應(yīng)該一致,但是很不幸,標準里雖然規(guī)定了 Error 對象是個 Ordinary Object,但也只規(guī)定了 name 和 message 兩個屬性的行為,對于最廣泛使用的 stack 屬性,并沒有加以定義,這導(dǎo)致了 JS 引擎在 stack 屬性的表現(xiàn)差別很大(目前已經(jīng)有一個標準化 stack 的 proposal),甚至有的引擎實現(xiàn)已經(jīng)突破了標準的限定,使得 Error 表現(xiàn)的更像一個 Exotic Object。我們來具體看看各引擎對于 Error 對象的實現(xiàn)差異。

V8 支持了 stack 屬性,并且給 stack 屬性提供了豐富的配置,如下是一個基本的 stack 信息。

function inner({
  myUndefinedFunction();
}
function outer({
  inner();
}
function main({
  try {
    outer();
  } catch (err) {
    console.log(err.stack);
  }
}

main();

我們可以使用 https://github.com/GoogleChromeLabs/jsvu 來很方便的測試不同 JS 引擎的表現(xiàn)差異


V8 的 stacktrace默認最多展示 10 個 frame,但是該數(shù)目可以通過 Error.stackLimit 進行配置,同時 V8 也支持了 async stacktrace,默認也會展示 async|await 的錯誤棧。

stacktrace 的捕獲不僅僅可以在出現(xiàn)異常時觸發(fā),還可以業(yè)務(wù)主動捕獲當前的 stacktrace,這樣我們就可以基于此實現(xiàn)自定義 Error 對象。

Error.captureStackTrace

V8 提供了 Error.captureStackTrace 支持用戶自定義收集 stackTrace。

這個 API 主要有兩種功能,一個是給自定義對象追加 stack 屬性,達到模擬 Error 的效果

function CustomError(message{
  this.message = message;
  this.name = CustomError.name;
  Error.captureStackTrace(this); // 給對象追加stack屬性
}

try {
  throw new CustomError('msg');
catch (e) {
  console.error(e.name); // CustomError
  console.error(e.message); //msg
  console.error(e.stack); 
  /*
    CustomError: msg
    at new CustomError (custom_error.js:4:9)
    at custom_error.js:7:9
  */

}

另一個作用就是可以隱藏部分實現(xiàn)細節(jié),這一方面可以避免一些對用戶無用的信息泄露給用戶,而對用戶造成困擾;另一方面可能有一些內(nèi)部信息涉及一些敏感信息,需要防止泄露給用戶。比如一般用戶是不需要關(guān)心 native 的調(diào)用棧,因此就需要將 native 的調(diào)用棧進行隱藏。下面的例子就簡單的演示了如何通過 captureStackTrace 來隱藏部分調(diào)用棧信息。

function CustomError(message, stripPoint{
  this.message = message;
  this.name = CustomError.name;
  Error.captureStackTrace(this, stripPoint);
}

function leak_secure({
  throw new CustomError('secure泄漏了');
}

function hidden_secure({
  throw new CustomError('secure沒泄露', outer_api);
}

function outer_api({
  try {
    leak_secure();
  } catch (err) {
    console.error('stk:', err.stack);
  }
  
  try {
    hidden_secure();
  } catch (err) {
    console.error('stk2:', err.stack);
  }
}

outer_api();

Error.prepareStackTrace

另一個值得注意點的是,雖然 stack 名義上應(yīng)該是一個 frame 的數(shù)組,但是實際上 stack 卻是個字符串(歷史包袱,兼容性問題吧),因此 V8 同時提供了一個結(jié)構(gòu)化的 stack 信息,方便用戶根據(jù)結(jié)構(gòu)化的 stack 信息來自定義 stack 結(jié)構(gòu)。我們可以通過 Error.prepareStackTrace 來獲取原始的棧幀結(jié)構(gòu):

Error.prepareStackTrace = (error, structedStackTrace) => {
  for (const frame of structedStackTrace) {
    console.log('frame:', frame.getFunctionName(), frame.getLineNumber(), frame.getColumnNumber());
  }
};

其中的 structedStackTrace 是個包含了 frame 信息的數(shù)組,其中包含了很多我們感興趣的信息,更詳細的信息可參考 stack-trace-api。

另外 stack 雖然是個 value property ,但是實際表現(xiàn)卻是個 getter property,這也是 V8 的實現(xiàn)違反 EcmaScript 規(guī)范的地方。


這里的 stack 雖然是個 value,但是其表現(xiàn)其實更像是一個 getter,因為其訪問 stack 的屬性會觸發(fā) prepareStackTrace 回調(diào)。這導(dǎo)致 error.stack 實際上是可能存在副作用的。不僅如此, stack 屬性的計算也是惰性計算的,當 error 觸發(fā)的時候并不會進行 stack 的計算,而只有當首次訪問 stack 的時候才會觸發(fā)計算,因為本身 stack 的計算實際上是有一定的性能開銷的,實際上 chrome devtools 就因為 stackstrace 的性能問題出過問題(faster-stack-trace),筆者也因為 stack-trace 的性能問題導(dǎo)致嚴重影響了編譯工具的編譯性能(https://github.com/evanw/esbuild/issues/1039)。

Stack Trace Format

前面已經(jīng)提到 V8 的 stack 是個字符串,如果需要將一個字符串解析為一個結(jié)構(gòu)化數(shù)據(jù),勢必該字符串需要符合某個格式規(guī)范,幸運的是 V8 的有一套格式規(guī)范,具體格式可見 stack-trace-format。雖然 V8 引擎定義了一套格式規(guī)范,不幸的是其他引擎的 stack 格式規(guī)范與 V8 截然不同(不帶重樣的),我們來看看不同引擎的格式規(guī)范。

V8(Chrome)


SpiderMonkey(Firefox)


JavaScriptCore(Safari)


QuickJS


我們發(fā)現(xiàn)四個 JS 引擎的 stack 格式各不相同,因此需要我們在上報錯誤前需要將這些格式進行分別的格式化處理,幸運的是 Sentry Client 已經(jīng)幫我們給主流的 JS 引擎做了適配。

sentry-compute-stack-trace


不幸的是,如果你的 JS 引擎是自研的或者 stack 格式和上述的引擎都不一致,你可能需要修改 Sentry 加以支持。

這里我們還發(fā)現(xiàn)了一個比較嚴重的問題就是 QuickJS 引擎的報錯是沒有行列號信息的,這對于平時的開發(fā)可能足夠了,但是如果你將你的代碼壓縮成一行,那么就會導(dǎo)致行列號信息都被丟失了,那么上報的錯誤是根本沒法進行反解的。更加不幸的是 QuickJS 至今仍然不支持列信息,如果你的系統(tǒng)里使用了 QuickJS,可能需要修改 QuickJS 自行添加列號支持。

eval is eval

如果你的代碼是在 eval 里執(zhí)行,那么將會碰到另一個問題,有的 JS 引擎針對 eval 的代碼并不會包含錯誤的行列號信息。

const code = `function inner() {
  myUndefinedFunction();
}

function outer() {
  inner();
}

function main() {
  try {
    outer();
  } catch (err) {
    console.log(err.stack);
  }
}

function foo() {
  bar();
}

function bar() {
  main();
}

foo();

eval(code);

比如我們看一下不同引擎的結(jié)果

  • V8 雖然包含了行列號信息,但是堆棧里含有了兩個行列號信息,可能導(dǎo)致 sentry-client 識別出錯
  • JavascriptCore 則徹底沒有行列號信息

  • SpiderMonkey 雖然能夠顯示,但與非 eval 版本格式完全不同,很難兼容

其實為了解決 eval 的錯誤堆棧的行列號問題,我們可以借助 sourceURL 進行還原,我們來看一看結(jié)果

const code = `function inner() {
  myUndefinedFunction();
}
function outer() {
  inner();
}
function main() {
  try {
    outer();
  } catch (err) {
    console.log(err.stack);
  }
}

function foo() {
  bar();
}
function bar() {
  main();
}

foo()
//# sourceURL=my-foo.js 
`
// 這里通過sourceURL支持還原

eval(code);

V8:成功還原


JavaScriptCore:不支持


SpiderMonkey:成功還原


anonymous function is bad

解決了 eval 的問題后,似乎可以高枕無憂了,但是實際開發(fā)中仍然碰到了一些匪夷所思的問題,線上反解仍然失敗,后來定位發(fā)現(xiàn) JavaScriptCore 在生成異常的時候,其行列號可能計算錯誤,以及給 QuickJS 引擎添加的行列號功能也存在不少 bug。那么在行列號不靠譜的情況下如何查問題。那就只能借助于 function name 去全局搜索了,不幸的是我們的業(yè)務(wù)中和 SDK 中存在大量的匿名函數(shù),這無異于雪上加霜。


對 YDKJS 的觀點深感贊同,不幸的是 JavaScript 里將 anonymous function 和 lexical this 兩個 feature 糅合在一起了,你除了通過變量聲明的方式,沒有其他更簡潔的方式來給一個 arrow function 進行命名

const xxx = () => {} // xxx.name is xxx
call('login', () => {
   this.crash()
}) // 如果這里crash了,很不幸調(diào)用棧里拿不到函數(shù)名了

const cb = () => {this.crash()}
call('login', cb) // 太繞了。。。。

call('login'function loginCb(){
   this.crash()
}.bind(this)) // 還是很繞

call('login', loginCb() => { // 這樣就最好了,可惜不支持,we need a proposal 
  this.crash();
})

如果 arrow function 也能便捷的命名就好了。

SourceMap 的局限

代碼反解只是 SourceMap 的一個功能,其實還扮演著源碼調(diào)試等功能,但是 SourceMap 在源碼調(diào)試上卻面臨著更大的問題和挑戰(zhàn),比如難以應(yīng)對其他高級語言的轉(zhuǎn)換問題,例如 C++ 編譯到 asm.js 或者 C++編譯為 wasm,如何處理 wasm 或者 asm.js 的源碼調(diào)試和代碼反解,是另一個比較復(fù)雜的話題了。

參考文獻

  • https://github.com/Rich-Harris/vlq/tree/master/sourcemaps
  • https://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html
  • https://tc39.es/ecma262/#sec-error-objects
  • getOwnPropertyDescriptor side effects
  • https://bugs.chromium.org/p/v8/issues/detail?id=5834
  • https://hacks.mozilla.org/2018/01/oxidizing-source-maps-with-rust-and-webassembly/





瀏覽 124
點贊
評論
收藏
分享

手機掃一掃分享

分享
舉報
評論
圖片
表情
推薦
點贊
評論
收藏
分享

手機掃一掃分享

分享
舉報

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 色网站在线观看| 性爱乱伦视频| 囯产精品久久久久久久久| 成人无码免费一区二区中文| 操老女人逼视频| 5252a我爱haose01我愿| 黄网国产手机在线观看| 亚洲一区视频| 久久久久成人片免费观看蜜芽| 少妇白洁在线观看| 欧美A片网站| 996re| 999福利视频| 精品九九九九九九| 波多野吉衣视频| 久久精品在线视频| 91蝌蚪| 亚洲无码成人在线| 9l视频自拍蝌蚪9l成人蝌蚪 | 日本爱爱片| 亚洲午夜AV| 五月色综合网| 日本三级网址| 青青草原在线视频| 伊人一区| 黃色一级A片一級片| 99色网站| 国产精品无码7777777| 极品一线天小嫩嫩真紧| 影音先锋在线成人| 国产精品久久久久久无人区| 国产乱码精品一品二品| 简单av网| 黄色网址在线观看视频| 精品一区二区三区av| 亚洲精品乱码久久久久久按摩观 | 99热综合| 91久色| A片在线免费看| 免费的操逼视频| 久久精品亚洲无码| 无码一区二区三区四区五区六区 | 亚洲尤物在线| 国产精品av在线播放| 中国AV网| 亚洲日本无码50p| av在线观看网站| 亚洲v欧美v| 操极品美女| 无码中文字幕| 久久肏| 国产精品秘久久久久久免费播放| 欧美激情一区二区三区| 91在线精品一区二区| 亚洲中文网| 黄片视频在线免费观看| 中文字幕黄色电影| 911久久| 国产日本在线| 西西人体444rt高清大胆模特| 香蕉AV777XXX色综合一区| 欧美久久免费| 日韩无码视频网| 欧美自拍| 国产成人午夜精品无码区久久麻豆 | 可以免费观看的毛片| 国产精品国产三级国产AⅤ中文| 黄色综合网站| 丁香婷婷五月基地| 色婷久久| 午夜成人网站在线观看| 伊人影院99| 麻豆传媒免费观看| 黄色a级毛片| 波多野结衣在线观看一区二区| 五月激情六月婷婷| 免费观看一级毛一片| 久久波多野结衣一区二区| 亚洲中文第一页| 九久热| 91久久无码一区人妻A片蜜桃| 欧美日韩网| 波多野结衣AV在线观看| 国产三级黄色| 久久久久久伊人| 少妇精品久久久久久久久久| 成人伊人电影| www.18av| 天堂网AV在线| 国产精品蜜| 青春草免费视频| 日韩AV免费电影| 国产一级婬片A片AAA樱花| 无码视频一区二区三区| 国产成人无码一区二区| 青春草在线观看视频| 操老女人的逼| 成人免费a片| 香蕉久久久| 国产欧美熟妇另类久久久| 欧美福利导航| 无码中文字幕| 久久黄色免费看| 精品国产自| 内射学生妹J亅| 无码高清18| 青娱乐在线精品| 91.xxxx| 99久久国产热无码精品免费| 3d动漫精品一区二区三区在线观看 | 高清无码人妻| 成人精品18| 国产性爱自拍一下| 九色九一| 国产SUV精品一区二区| 青青久久91| 一级AAAAA片裸体做受| 成人性爱视频网| 成人免费精品视频| 亚洲午夜免费视频| 狠狠操狠狠操狠狠操| 国产熟妇婬乱A片免费看牛牛| 国产成人AV网站| 国产激情小视频| 先锋影音资源站| 成人先锋AV| 99在线精品视频在线观看| 2024av在线| 黑人乱伦| 五月天精品| 十八禁黄网站| 中文字幕精品亚洲熟女| 欧美综合激情| 欧美综合网| 亚欧av无码| 国产人妻精品一区二区三区不卡| 操逼手机视频| 五月网婷婷| 99国产热| 成人看片黄a免费看视频| 噜噜色色噜噜| 午夜天堂| 青青草成人免费在线视频| 亚洲欧美久久久久久久久久久久| 一本一道久久综合| 日产精品久久久一区二区| 国产黄色片视频| 国产色情在线观看| 苍井空一区| 五月天婷婷乱伦| 可以免费观看的AV| 黄色视频大全在线观看| 亚洲国产成人在线视频| 日韩av中文字幕在线| 欧美一级黃色A片免费看小优视频 无码人妻精品一区二区三千菊电影 | 国产成人久久777777黄蓉| 日本一区二区三| 国产又粗又猛又黄又爽无遮挡| 青青草原国产视频| 亚洲AV无码永久精品| 国产在线高潮| 色播综合| 殴美色色网| 国产卡一卡二在线观看| 国产污视频在线观看| 日韩人妻精品中文字幕专区不卡| 五月在线视频| 亚洲AV免费在线| 亚洲不卡| 国产资源网| 精品伊人久久| 三级国产AV| 嫩BBB搡BBB槡BBB小号| 天天拍天天干| 国产天堂视频| 色婷久久| 欧美国产日韩在线观看| 中文字幕在线观看AV| 一区二区在线视频| а√在线中文网新版地址在线 | 乱伦五月| 91精品导航| 97超碰免费| 久久亚洲Aⅴ成人无码国产丝袜| 成人福利视频在线| 日韩中文字幕在线免费观看| 91操B| 欧美精品一区二区三区使用方法| 国产精品久久久91| 亚洲免费AV在线| 亚洲午夜免费视频| 九九热精品视频在线观看| H片在线观看| 精品人妻一区二区三区日产乱码 | 久操久操| 久久国产av| 波多野结衣AV在线| 特级西西西西4444级酉西88wwww特| 北条麻妃无码| 无码毛片在线观看| 大炕上公让我高潮了六次| www.久久久久| AAA黄片| 丁香婷婷五月综合影院| 啪啪视频在线观看| 人妻体内射精一区二区三区| 麻豆乱码国产一区二区三区| 成人做爰100部片视频| 午夜传媒一区二区三区| 操综合网| 成人一区二区三区四区| 国产手机AV在线| 在线无.码| 国产成人免费做爰视频| 五月天性爱视频| 无码伦理电影| 婷婷五月天丁香在线| 国产嫩BBwBBw高潮| h无码| 日屄视频免费看| 豆花成人视频在线观看| 免费操逼视频在线观看| 97福利视频| 国产欧美在线免费观看| 国外成人性视频免费| 97超碰人妻| 老司机永久免费91| 免费看黄色的网站| 国产亚洲精品码| 免费的av| 免费视频在线观看黄| 国产成人精品av在线观看| 良妇露脸15P| 亚洲欧洲无码在线| 亚洲免费大片| 狼人狠狠干| 欧美黄色电影在线观看| 国产成人69免费看| 欧美成人精品欧美一级| 你懂的视频在线| 午夜av免费| 成人性爱在线视频| 激情五月丁香婷婷| 99精品欲| 成人网大香蕉| 婷婷在线观看视频| 一二三区| 男人天堂综合网| 无码一区二区三区在线| 插菊花综合网亚洲| 开心激情网五月天| 重庆美女揉BBBB搡BBBB| 亚洲欧美激情视频| 色婷婷在线影院| 91在线视频精品| 黄网在线观看视频| 北条麻妃AV在线播放| 亚洲精品在线观看视频| 欧美偷拍一区二区| 俺去也www俺去也com| 久久精品小视频| 人人澡人人爽欧一区| 欧美日韩不卡视频| 強姦婬片A片AAA毛片Mⅴ| 天堂在线观看AV| 色婷婷中文| 国产三级在线播放| 在线成人毛片| 国产精品1区2区| 免费av网站| 国产成人三级视频| 激情AV| 亚洲AV无码国产精品二区| 黄a在线| 东北毛片| 亚洲播播在线视频| 成人亚洲在线| 自慰喷水流白浆中文字幕| 艹美女视频| 丝袜足交视频| 中文字幕播放| 天天免费视频| 精品成人| 婷婷五月综合中文字幕| 蜜桃AV一区二区三区| 国产在线久久久| 国产成人AV免费无码| 成人无码中文字幕| 18精品爽视频| 国产精品视频免费看| 久久精品免费看| 熟女国产| 日韩视频中文字幕| 成人综合娱乐网| 口爆av| 伊人综合成人网| 国产3级片| 操逼视频网| 人妻精品综合码| 天天视频色| www.yw尤物| 日本一级黄色电影网| 成人AV片导航| 91精品无码| 中文字幕在线观看完整av| 蜜臀网| 日本黄色视频免费观看| 蜜桃性视频| 大鸡吧视频在线观看| 国产欧美二区综合中文字幕精品一| 精品无码人妻一区二区三区| 丁香五月网| 九色91PORNY国产| 五月激情网站| 欧美激情一区二区A片成人牛牛| 日逼网站免费观看| 在线亚洲免费观看| 熟女视频91| 国产欧美在线综合| 日韩无码精品一区二区三区| 亚洲三级在线观看| 青青草伊人大香蕉| 操逼免费观看| 国产在线播放av| 亚洲无码视频免费看| 久久av影院| 精品视频| 高清无码成人视频| 日韩精品五区| a片网站在线观看| 福利久久| 国产成人V在线精品一区| 91久久国产综合久| 免费观看日韩无码视频| 操比片| 91在线看18| 国产情侣在线视频| 国产黄色片网站| 中文字幕第69页| 欧美偷拍一区二区| 国产三级国产三级国产| 国产在线观看97| 国产三级在线观看视频| 天天操天天干麻豆| 久久XX| 亚洲偷拍中文| 亚洲无码成人片| 裸体黄色一极大片| 欧美一級黃色A片免費看| 激情网站免费| 俺去啦在线视频| 午夜无码免费| AV资源在线播放| 国产精品国产三级国产专区52 | 午夜福利在线播放| 西西888WWW大胆无码| 香蕉视频日韩| 大香蕉一区二区三区| 超碰免费99| 99热超碰| 51AV在线| 操逼逼一区二区三区| 国产豆花视频| 天天天做夜夜夜夜爽无码| 国产嫩草精品A88AV| 国产精品一级二级三级| 国产成人视频免费观看| 亚洲日本一区二区三区| 琪琪色视频| 91乱了伦国产乱子伦| 91在线无码精品秘入口动作| 香蕉视频日韩| 麻豆乱伦| 天堂a在线| 国产熟女一区二区三区五月婷 | 亚洲国产精品成人综合色在线婷婷 | 国产剧情自拍| 成人在线超碰| 国产性生活视频| 亚洲一级黄色| www.豆花福利视频| 一级黄色毛片| 亚洲女人在线| 亚洲国产av一区| 极品少妇AV| 成年人免费毛片| 欧美在线a| 国产乱人伦无码视频| 久久一做爱| 熟女视频91| 无码人妻精品一区二区三区温州 | 午夜在线视频| 欧美人成人无码| 免费射精一二三区| 安徽妇搡BBBB搡BBBB袄爱直播 | 91视频成人版一区二区| 中文字幕23页| 91区视频| 狼友视频在线| 操学生妹| 女同一区二区三区| 久热网| 日本成人一区二区三区| 91在线看| A片黄色电影网站| 日日视频| 国产成人精品无码免费| 自拍偷拍视频网| 人人操人人| 91麻豆精品传媒| 国产91麻豆视频| 欧美日本黄色| 天天干天天射天天爽| 三级片日本在线| 久操无码| 在线播放91灌醉迷J高跟美女 | 亚洲最大福利视频| 伊人二区| 亚洲资源在线观看| 国内自拍偷拍视频| 视频在线a| 西西4444www大胆无| 日本黄A三级三级三级| 人人操在线| 国产成人视频免费| 牛牛无码| 国产熟妇婬乱一区二区| 天天干天天干天| 久久成人18免费网站波多野结衣 | 最近中文字幕高清2019中文字幕| 国产毛片网| 色五月电影| 福利一区二区| 久久永久视频| 免费无码国产| 中文字幕免费在线观看| 国模一区二区| 久草高清视频| 亚洲色吧| 97欧美| 玖玖爱这里只有精品| 久99| 97久久超碰| 日韩TV| 制服丝袜大香蕉| 老汉av| 殴美色色网| 天堂操逼| 日本婷婷| 波多野结衣成人网站| 韩日一区二区| 12—13女人毛片毛片| 日韩丰满人妻| 国产在线第一页| 午夜福利小视频| 大学生18一19GAY169| 欧美三级片视频| 欧美色址| 久久另类TS人妖一区二区免费| 91成人片| 91精品视频网站| 亚洲精品秘一区二区三区影| 欧美激情视频在线| 91在线超碰| 成人性爱在线视频| 在线91网站| 免费在线观看黄| 色色视频在线观看| 免费涩涩无遮挡18国产| 人人爱人人妻人人操| 亚洲中文字幕码mv| 影音先锋乱伦| 西西444WWW无码大胆在线观看 | 久久澡| 操逼在线视频| 黄片网页| 无码蜜桃吴梦梦| 日P免费视频| 亚洲永久免费| JlZZJLZZ亚洲美女18| 国产最新AV| 国产亚洲视频完整在线观看| 中文字幕免费高清在线观看| 特级西西444WWW高清| 少妇三级| 国产aa| 色婷婷在线视频观看| 人人澡视频| 国产无码内射视频| 国产精品操逼| 国产精品成人无码a无码| 九色首页| 中文字幕一级片| 天天干天天日天天操| 国产一级片在线播放| 一线毛片| 黄色视频A片| 女同二人91| 亚洲日韩Av无码中文字幕美国| 蜜桃视频网址| 久9久9久9久9久9久9| 国产浮力草草| 日本黄色免费| 狠狠综合网| 中文字幕天堂在线| 免费无码一区二区三区四区五区 | 亚洲中文无码在线| 午夜电影无码| 人妻斩り43歳| 另类老妇奶性生BBwBBw偷拍| Al激情欧美| 免费黄色A片| 成人午夜黄片| 91大神shunv| 亚洲日韩精品在线视频| 99精品视频在线观看| 亚洲色情在线| 亚洲天堂无码高清| 99精品自拍| 国产精品午夜在线观看| 亚洲精品乱码久久久久| 欧美日韩中文字幕在线| 日韩午夜电影| 欧美内射在线| 亚洲激情综合视频| 成人av黄色三级片在线观看 | 欧美日韩男女淫乱一区二区| 日本色色网| 欧美午夜精品成人片在线播放| 麻豆自拍偷拍视频| 亚洲无码AV免费观看| 欧美亚洲国产精品| 伊人成人在线观看| 91日逼视频| 日韩爱爱爱| 无码一区二区高清| 人妻丰满熟妇| 中文字幕高清免费看| 国产伦子伦一级A片免费看小说 | 亚洲高清无码免费观看| 97人人干人人| 国产一区二区三区四区五区六区七区| gogogo高清在线完整免费播放韩国 | 美女福利在线| 伊大香蕉| 91精品国产91久久久久久吃药| 国产18女人水真多免费看| 天天操天天干天天日| 韩国一级av| 亚洲高清无码免费观看| 国产毛片久久久久久久| 免费观看黄色AV| 久久久无码人妻精品无码| 成人精品| 欧美亚韩一区二区三区| 国产一级片在线播放| 91视频网站在线| 色黄网站在线观看| 日韩无码流出| 午夜偷拍网站| 淫荡五月天视频导航| 国产精品无码一区二区在线欢| 啪啪视频免费观看| 九九99热| 3d动漫精品一区二区三区在线观看 | 天天干中文字幕| 国产第一页在线播放| 日本亚洲中文字幕| 国产日逼片| 国产精品国内自产| 国精产品秘一区二区| 乱伦视频网| 一区二区三区电影高清电影免费观看| xxx日韩| 亚洲日韩免费视频| 中文字幕有码在线看| 亚洲无码中文字幕在线| 久久精品99国产国产精| 精品国产自| MAD033_后宫秘密陶子.| 亚洲天堂无码在线| 毛片毛片毛片毛片| 日本丰满老熟妇乱子伦| 日本高清无码在线观看| 成人做爰黄A片免费视频网站野外| 欧美日屄视频| 韩日综合在线| 国产色自拍| 99久久爱re热6在播放| 精品久| 黄色片免费| 免费看一级一级人妻片| 嫩草在线视频| 丰满岳乱妇一区二区三区| 亚洲色爽| 2021天天操| 视频你懂的| 蜜臀AV在线观看| 一卡二卡三卡无码| 亚洲中文字幕第一| 91麻豆天美传媒在线| 日韩中文毛片| 黄片视频免费| 欧美日本国产| 国产超碰在线| 99精品视频在线播放免费| 国产色情视频| 无码视频一区二区三区| 五月婷婷激情综合| 国产成人精品久久久| 操逼中文字幕| 欧美婬乱片A片AAA毛片地址| 亚洲精品一区二区三区四区五区六区| 一区二区三区四区| 肏屄在线观看| 2021国产精品视频| 精品网站| 国产伦精品一区二区三区色大师 | 波多野结衣vs黑人巨大| 亚洲无码高清在线观看视频| 久久电影五月天| 麻豆三级| 97精品视频| 老骚老B老太太A片| 久久国产乱子伦精品免费女,网站| 日韩精品无码一区二区| 青青草原网| 九九国产视频| 亚洲AV第二区国产精品| 亚洲中文AV| 亚洲无码免费在线| 成人做爰黄A片免费看直播室动漫 中文字幕一区二区三区四虎在线 欧美熟妇精品一级A片视色 | 88AV在线视频| 韩国三级HD中文字幕的背景音乐 | 中文字幕在线资源| 99久久婷婷国产综合精品草原| 中文天堂网| 成人国产精品| 国产丨熟女丨国产熟女视频| 中文字幕不卡在线观看| 成人在线小视频| 老司机免费福利视频| 97人人草| 一区二区三区高清无码| 成年人黄色片| 人人妻人人澡人人爽人人DVD | 91人妻无码一区二区久久| 精品AV无码一区二区三区| 久久成人三级| 在线观看AV无码| 国外亚洲成AV人片在线观看| 乱伦99| 日韩欧美国产黄色电影| 国产精品自拍一区| 亚洲久久色| 5252a我爱haose01我愿| 亚洲AV片一区二区三区| 亚洲码AV波多野| 欧美日韩大屌| 午夜国产在线视频| 337p粉嫩噜噜噜| 天天日天天草天天干| 18禁网站免费观看| 成人综合在线观看| 竹菊传媒一区二区三区| 五月停亭六月,六月停亭的英语 | 91西安站街老熟女露脸| 久久精品999| 色综合久久久无码中文字幕999| 日韩h视频| 超碰在线网站| 3344在线观看免费下载视频 | 国产视频黄| 综合天天| 一级A级毛片| 99精品视频免费看| www.色悠悠| 91中文字幕+乱码| www.人人操| 中文字幕一区二区三区的重点问题| 777777视频| 91人妻成人精品一区二区| 欧美日韩三级片| av拍拍| 九九九成人视频| 中文字幕视频在线直播| 又大又长又粗91| 狠狠操狠狠插| 欧美日韩在线视频免费播放| 秘蜜桃色一区二区三区在线观看| 天堂网一区二区三区| 好男人WWW社区在线视频夜恋| 亚洲国产成人在线视频| 91人人操人人爽| 人人看人人摸人人搞| AV在线一区二区| 午夜成人AV| 在线免费观看AV片| 东北毛片| 国产一区二区精品| 日鸡吧链接| 午夜久操| 91新婚人妻偷拍| 黄色小视频免费| 婷婷三级| 乱子伦国产精品视频一级毛| 天天爽天天日| 欧美性受| 亚洲在线视频播放| 国产精品9| 强开小嫩苞一区二区三区视频| 中文字幕高清在线中文字幕中文字幕 | 天天视频色| 51成人网站| 婷婷激情五月| 亚洲欧美精品AAAAAA片| AV口爆| 啪啪免费| 亚洲无码精品专区| 日本无码成人| 中文日韩在线| 91色婷婷综合久久中文字幕二区| 五月婷婷欧美| 人人操人人妻| 欧美东京热视频| 久久久久久久久久久久成人| 人人摸在线视频| 熟女少妇视频| 少妇搡BBBB搡BBB搡造水多| 大香蕉网伊人| h网站在线观看| 高清一区二区| 亚洲成人自拍无码| 青青操天天干| 亚洲精品乱码久久久久久久| 一级片a片| 欧美成人激情视频| 久久国产av| 国产激情视频在线| 伊人久综合| 超碰碰人人| 超碰2023| 四川少妇BBBB| 久久艹视频| 亚洲中文字幕一区| 久草视频99| 久久亚洲AV无码午夜麻豆| 美女裸身18禁| 麻豆一区在线观看| 97人妻在线视频| 大鸡吧在线观看| 欧美一二三区黄色免费视屏| 超碰在线免费播放| 九七在线视频| 欧美自拍| 国产无码在线看| 3D动漫精品啪啪一区二区免费| 欧美激情伊人久久五月天| 亚洲一本色道中文无码| 精品三级在线观看| 久久久青草| 99九九久久| 婷婷久久五月| 欧美色大香蕉| 亚洲日韩精品成人无码专区AV| 国产精品你懂得| 成人啪啪网站| 亚洲www.| 青青草五月天色婷婷丁香| 欧美日韩一区二区三区四区五区六区| 九一无码| 四色永久成人网站| 一道本AV| 成人黄色在线观看| 青青操人人操| 国产香蕉视频在线播放| www.热久久| 日本中文字幕在线视频| 婷婷激情综合| 国产极品久久久| 亚洲精品二| 国产色网站| 精品人妻一区二区三区-国产精品 无码人妻av黄色一区二区三区 | 翔田千里av| 亚洲中文字幕av| 西西4444www大胆无吗| 天天操天天日天天射| 九九精品视频在线观看| www.黄色视频| 乱伦无码高清麻豆视频一区二区 | 亚洲日本在线观看| 国产乱码在线| 91视频福利| 色婷婷国产精品综合在线观看| 老鸭窝在线观看视频| 国产成人高清| 美女毛片网站| 久久免费播放视频| 国产精品乱子伦视频一区二区 | 日韩一级片免费| 久久综合久久鬼| 蜜臀久久99精品久久久巴士| 无码成人网| 日本视频精品| 亚洲AV成人精品一区二区三区| 日本无码一区二区| 久久网一区| 摸BBB搡BBB搡BBBB| 成人无码人妻| 成年人视频网站| 996精品在线| 久久大陆| www.国产精品| 国产A片录制现场妹子都很多| 国产又粗又大又爽| 欧美1区2区| 免费看无码一级A片在线播放| 中文爱爱视频| 久久一级A片| 伊人蕉久| 强奸乱伦制服丝袜| 色婷婷亚洲色| 国产精品你懂得| 18岁毛片| 国产曰韩欧美综合另类在线| 亚洲天堂在线免费观看视频| 国产色情视频在线观看| 2017人人操| 四虎影库男人天堂| 青青草黄色视频| 男人色天堂网| 免费无码视频| 国产一区二区波多野结衣| 国产黄色免费电影| 国产久久在线| 无码伦理电影| 亚洲AV成人一区二区三区不卡| 91麻豆精品91久久久ios版| 免费无码高清| 国产成人自拍视频在线| 国产3级片| 88av在线| 五月天亚洲无码| A天堂视频| EEUSS| 婬乱欧美一二三区| 久久伊思人在| 久久午夜无码鲁丝片午夜精| 精品久久久久久AV2025| 日日爽夜夜| 成人做爰100部免费网站| 黄色激情网站| 日韩一级在线免费观看| AV无码观看| 91成人在线视频| 日韩综合精品| 色99视频| 黄色视频在线观看18| 538在线观看| 可以在线观看的av| 国产日韩欧美在线观看| 久一在线| 青青青草视频在线| 亚洲aaa| 日韩無码专区| 欧美黄色免费观看| 久久嫩草在线影院| 亚洲精品二| 国产在线观看av| 一级A片视频免费看| 日韩精品成人av| 91精品视频在线播放| 日色色色| 大香蕉老师| 伊人久久婷婷| 97超碰在线视| 欧美成人精品在线观看| 一级片在线| av影音先锋在线| 亚洲人成无码| 中文字幕淫乱视频欧美| 制服丝袜在线视频| 在线观看禁无码精品| 亚洲日韩网站在线观看| 国产裸体网站| 日逼国产| 亚洲无码理论片| 91ThePorn国产在线观看| 午夜毛片| 91乱| 亚洲无码在线播放| 翔田千里在线一区二区三区| 内射视频在线免费观看| 国产成人精品AV在线观| 竹菊av一区二区三区四区五区 | 樱桃码一区二区三区| 九九re精品视频在线观看| AV中文字幕在线播放| 亚洲人内射片又| 久艹综合|