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

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

共 17124字,需瀏覽 35分鐘

 ·

2021-10-18 11:52

背景

我們從事 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 開始

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

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


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

代碼反解

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

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

SourceMap

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

SourceMap 支持的全鏈路流程

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

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

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)過壓縮處理的代碼對應不上了。

??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)了代碼出問題了,我們希望錯誤的堆棧能顯示源碼的位置,另外能支持源碼調試應用,這時候就需要用的瀏覽器的 SourceMap 支持和 node 的 SourceMap 支持了。

日志收集和上報

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

錯誤日志反解

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

總結一下,一個完整的 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é)的出錯都可能導致你線上的錯誤日志反解前功盡棄,你所能做的就是在整個鏈路上進行分析定位。

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

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",?//?轉換后的文件名
???sourceRoot?:?"",?//?轉換前的文件所在目錄,如果與轉換前的文件在同一目錄,該項為空
???sources:?["add.ts"],?//?轉換前的文件,該項是一個數(shù)組,表示可能存在多個文件合并
???names:?[],?//?轉換前的所有變量名和屬性名,多用于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 實際上是個三級結構,我們以上述的例子為例

  • 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'?//?};
]

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

  • segment:每一行同包含由 , 分割的多個 segment 信息,其中每個 segment 都對應了產(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 都編碼了具體的行列映射信息,依次為
    • 第一位: 轉換后代碼所處的列號,如果這是當前行的第一個 segment,那么是個絕對值,否則是相對于上一個 segment 的相對值
    • 第二位:表示這個位置屬于 sources 屬性中的哪一個文件,相對于前一個 segment 的位置(區(qū)別于列號,下一行的第一個 segment 仍然是相對于上一行的最后一個 segment,并不會 reset)
    • 第三位:表示這個位置屬于轉換前代碼的第幾行,相對位置,同第二列
    • 第四位:表示這個位置屬于轉換前代碼的第幾列,相對位置,同第二列
    • 第五位:表示這個位置屬于 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)
}

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

[
??[
????[?0,?0,?0,?0?],
????[?4,?0,?0,?6?],
????[?3,?0,?0,?3?],
????[?3,?0,?0,?3?],
????[?10,?0,?0,?1?],
????[?1,?0,?0,?8?],
????[?2,?0,?0,?1?],
????[?1,?0,?0,?8?]
??],
??[
????[?4,?0,?1,?-28?],
????[?7,?0,?0,?7?],
????[?1,?0,?0,?1?],
????[?3,?0,?0,?1?],
????[?1,?0,?0,?1?],
????[?1,?0,?0,?1?]
??],
??[?[?0,?0,?1,?-13?],?[?1,?0,?0,?1?],?[?1,?0,?0,?0?]?]
]

還原絕對位置索引

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

??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?????????absSegment[i]?+=?segment[i];
????????result.push(absSegment[i]);
??????}
??????return?result;
????});
????return?absoluteSegment;
??});
??console.log('decoded:',?decoded)
}

結果如下,此時為絕對位置映射表

?[
??[
????[?0,?0,?0,?0?],
????[?4,?0,?0,?6?],
????[?7,?0,?0,?9?],
????[?10,?0,?0,?12?],
????[?20,?0,?0,?13?],
????[?21,?0,?0,?21?],
????[?23,?0,?0,?22?],
????[?24,?0,?0,?30?]
??],
??[
????[?4,?0,?1,?2?],
????[?11,?0,?1,?9?],
????[?12,?0,?1,?10?],
????[?15,?0,?1,?11?],
????[?16,?0,?1,?12?],
????[?17,?0,?1,?13?]
??],
??[?[?0,?0,?2,?0?],?[?1,?0,?2,?1?],?[?2,?0,?2,?1?]?]
]

雙向映射

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

originalPositionFor 用于根據(jù)產(chǎn)物的行列號,查找對應源碼的信息,而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?=?[0,?0,?0,?0,?0];
????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???????????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 相應的工具鏈都是基于此設計的,但是在給整個鏈路做 SourceMap 支持的時候,但是鏈路的每一步需要解決的問題卻各有不同(的坑),我們來一步步的研(踩)究(坑)吧。

給 transformer 添加 SourceMap 映射

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

  • AST 變換

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

import?babel?from?'@babel/core';
import?fs?from?'fs';
const?result?=?babel.transform('a?===?b;',?{
??sourceMaps:?true,
??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);
//?結果
b?===?a;?
{
??version:?3,
??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(0,?8,?'answer');
s.toString();?//?'answer?=?99'

s.overwrite(11,?13,?'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',
??includeContent:?true
});?//?generates?a?v3?SourceMap

console.log('code:',?s.toString());
console.log('map:',?map);
//?結果?
code:?var?answer?=?42;
map:?SourceMap?{
??version:?3,
??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',
??sourceMaps:?true,
??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:?{
??????includeSources:?true
????}
??}
);
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è)務中加以使用了,但是卻“驚喜”的發(fā)現(xiàn)整個構建流程的速度直線下降,因為 SourceMap 操作的開銷實際上是非常可觀的,在不需要 SourceMap 的情況下或者在對性能極其敏感的場景下(服務端構建),實際是不建議默認開啟 SourceMap 的,事實上 SourceMap 對性能極其敏感,以至于 source-map 庫的作者們重新用 rust 實現(xiàn)了 source-map,并將其編譯到了 webassembly。

錯誤日志上報和反解

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

Sentry

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

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)行為應該一致,但是很不幸,標準里雖然規(guī)定了 Error 對象是個 Ordinary Object,但也只規(guī)定了 name 和 message 兩個屬性的行為,對于最廣泛使用的 stack 屬性,并沒有加以定義,這導致了 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è)務主動捕獲當前的 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)部信息涉及一些敏感信息,需要防止泄露給用戶。比如一般用戶是不需要關心 native 的調用棧,因此就需要將 native 的調用棧進行隱藏。下面的例子就簡單的演示了如何通過 captureStackTrace 來隱藏部分調用棧信息。

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 名義上應該是一個 frame 的數(shù)組,但是實際上 stack 卻是個字符串(歷史包袱,兼容性問題吧),因此 V8 同時提供了一個結構化的 stack 信息,方便用戶根據(jù)結構化的 stack 信息來自定義 stack 結構。我們可以通過 Error.prepareStackTrace 來獲取原始的棧幀結構:

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 回調。這導致 error.stack 實際上是可能存在副作用的。不僅如此, stack 屬性的計算也是惰性計算的,當 error 觸發(fā)的時候并不會進行 stack 的計算,而只有當首次訪問 stack 的時候才會觸發(fā)計算,因為本身 stack 的計算實際上是有一定的性能開銷的,實際上 chrome devtools 就因為 stackstrace 的性能問題出過問題(faster-stack-trace),筆者也因為 stack-trace 的性能問題導致嚴重影響了編譯工具的編譯性能(https://github.com/evanw/esbuild/issues/1039)。

Stack Trace Format

前面已經(jīng)提到 V8 的 stack 是個字符串,如果需要將一個字符串解析為一個結構化數(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ā)可能足夠了,但是如果你將你的代碼壓縮成一行,那么就會導致行列號信息都被丟失了,那么上報的錯誤是根本沒法進行反解的。更加不幸的是 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);

比如我們看一下不同引擎的結果

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

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

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

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è)務中和 SDK 中存在大量的匿名函數(shù),這無異于雪上加霜。


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

const?xxx?=?()?=>?{}?//?xxx.name?is?xxx
call('login',?()?=>?{
???this.crash()
})?//?如果這里crash了,很不幸調用棧里拿不到函數(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 的一個功能,其實還扮演著源碼調試等功能,但是 SourceMap 在源碼調試上卻面臨著更大的問題和挑戰(zhàn),比如難以應對其他高級語言的轉換問題,例如 C++ 編譯到 asm.js 或者 C++編譯為 wasm,如何處理 wasm 或者 asm.js 的源碼調試和代碼反解,是另一個比較復雜的話題了。

參考文獻

  • 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/
瀏覽 82
點贊
評論
收藏
分享

手機掃一掃分享

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

手機掃一掃分享

分享
舉報

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

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 欧美三级在线观看视频| 99国产在线观看免费视频| 日本一级理论片在线大全| 久久精品福利视频| 精品综合| 手机AV在线观看| 日韩无码黄色片| 亚洲天堂久久| 久久成人综合| 内射无码视频| 中文字幕国产一区| 亚洲精品久久久久毛片A级牛奶| 69AV视频| 亚洲高清在线观看视频| 麻豆自拍偷拍| 99er在线观看视频| 亚洲天堂三级片| 伊人久久AV诱惑悠悠| 欧美激情五月| 日逼免费视频| 黄色电影网站在线观看| 奇米无码| 永久中文字幕| 国产美女18毛片水真多| 日韩中文字幕无码| 成人午夜福利电影| 围产精品久久久久久久| 亚州无码精品| 中文字幕+乱码+中文字幕一区| 亚洲欧洲无码在线| 人妻少妇精品视频| 先锋影音AV资源网| 男人天堂视频网| 嫩草久久99www亚洲红桃| 走光无码一区二区三区| 免费观看黄色成人网站| 成人小说视频在线社区| 亚洲性爱一区| 高清无码三级| 看a网站| 国产精品久久久久久无码人妻 | www.俺来也| 九九视频免费观看| 99热综合在线| 多啪啪免费视频| 亚洲综合中文字幕在线播放| 婷婷色777777| 操操操操操| 成人激情综合网| 日批视频在线观看| 久久私拍| 国产视频高清无码| 丝袜久久| 国产夫妻露脸| 精品九九九九九九| 国内自拍激情视频| 天天久久| 嫩BBB槡BBBB槡BBB3i| 国产AV一区二区三区精品| 永久免费av| 黄视频在线观看免费| 国产免费福利| 国产亚洲无码激情前后夹击| 丰满大爆乳波霸奶| 国产,亚洲91| 一区二区在线不卡| 一本大道香蕉av久久精东影业| www.五月婷婷| 三级亚洲| 午夜精东影业传媒在线观看| 午夜AV在线免费观看| 美日韩无码视频| 欧美大黄视频| 天天做天天爱夜夜爽| 做爱视频毛片人乱| 在线免费看AV片| 曰曰操| 色播婷婷五月天| 欧美不卡在线视频| 欧美色图在线视频| 操B视频网站| 免费三级网址| 青春草在线观看视频| 8x8x黄色| 91人妻最真实刺激绿帽| 精品一区二区三区免费| 无码人妻一区二区三区在线视频不卡 | 国产性爱在线| 久草这里只有精品| 成人AV免费在线观看| 亚洲精品成a人在线观看| 热久久免费| 99久久精品国产一区二区成人| 国产手机拍视频推荐2023| 国产91探花精品一区二区| 91在线无码精品秘入口电车| 11一12周岁女毛片| 91欧美精品成人AAA片| 福利视频中文字幕| 中文字幕一区三区人妻视频| 波多野结衣99| 国产成人综合亚洲| 西西444WWW无码视频软件功能介绍 | 国产三级片无码| 久草黄色| 久久午夜福利电影| 操逼在线看| caopro| 91人妻论坛| 亚洲第一页在线观看| 亚洲天堂精品在线观看| 天天干天天草| 日韩婷婷| 国产欧美一区二区三区视频| 亚洲欧美熟妇久久久久久久久| 败火老熟女ThePorn视频| 99热思思| 91最新视频| 特级西西WWW无码| 亚洲精品麻豆| 日本天堂在线视频| 精品伊人| 欧美肉大捧一进一出小说| 欧美成人第一页| 99色综合| 欧美综合国产| 91香蕉视频18| 91大神在线免费看| 久久久久久无码视频| 四川少妇bbbb| 国产对白视频| 强伦轩一区二区三区在线观看| 日韩国产| 激情操逼网| 欧美操逼的| 国产精品精品| 玖玖成人| 辽宁模特张雪馨视频最新| 国产精品熟女| 日本親子亂子倫XXXX| 人妻少妇综合| 九九九AV| 91麻豆精品国产91久久久吃药| 免费黄色视频观看| jizz在线观看视频| 国产香蕉视频| 麻豆疯狂做受XXXX高潮视频| 亚洲三级视频在线播出| 日韩黄色电影视频| 日韩一级片免费| 亚洲怡春院| 成人小说在线观看| 人人操人妻| 网站色色免费看| 国产,亚洲91| 91免费观看网站| 国产激情视频网站| 国产精品不卡在线观看| 国产黄色精品视频| 操逼99| 99热| 熟女人妻一区二区三区免费看| 西西人体444www| jt33免费观看高清| 四川BBBBBB搡BBBBB| 国产精品成人99一区无码 | 日本AI高清无码在线观看网址| 毛片黄片| 久久久高清无码视频| 香蕉伊人视频| 2025av天堂| 91蝌蚪| 国产香蕉视频| 鸭子av| 亚洲真人无码| 成人在线欧美| 91小仙女jK白丝袜呻吟| 精品三级片| 国产综合色婷婷精品久久| 日逼| 亚洲AV成人无码一区二区三区| 国产精品不卡在线观看| 免费亲子乱婬一级A片| 国产熟妇码视频户外直播| 久久网一区| 欧美成人三级在线播放| 亚洲乱码精品久久久久..| AV天堂亚洲| 婷婷色综合视频二区| 影音先锋女人aV鲁色资源网站| 51妺嘿嘿午夜福利视频| 欧美黄色免费网站| 国精产品一区二区三区黑人和中国 | 一本色道久久综合| 久久综合久久鬼| 丁香六月婷婷综合| AV在线播放中文字幕| 日本黄色片| 无码精品一区二区三区在线观看 | 中文字幕视频2023| 中文字幕线观看| 粉嫩AV蜜乳AV蜜臀AV蜂腰AV | 日韩三级| 成人无码自拍| 国产亚洲欧美视频| 一级黄色电影免费在线观看| sm在线观看| 国产在线拍揄自揄拍无码福利| 狠狠综合| 嘿咻嘿咻动态图| 激情五月色五月| 青春草在线视频免费观看| 大地资源第三页在线观看免费播放最新 | 一道本视频在线免费观看| 自慰影院| 亚洲欧美日韩综合| 丁香五月综合| 青青日逼| 无码黄页| 一级爱爱免费视频| 黄色电影天堂网站| 亚洲成人在线观看视频| 国产激情视频在线播放| 五月天黄色电影| 亚洲中文字幕无码在线观看| 奇米影视77777| 黄色小视频在线观看| 久久久久久无码日韩欧美电影| 天天操夜夜操狠狠| 爱爱成人视频| 久操大香蕉| www.啪| 99热精品国产| 欧美精品综合| 四季AV一区二区凹凸懂色桃花| 国产AV无码精品| 国产精品无码成人AV在线播放 | 2024无码| 日韩无码影视| 国产激情AV| 精品多人P群无码视频| 大香蕉网在线| 操毛| 色哟哟视频在线观看| 黄色片视频免费| 极品一区| 国产成人三级在线| 久久久青草| 免费69视频| 国产拍拍视频| 日本一本在线| 国产艹逼| 欧美精品国产动漫| 激情人妻av| 天天干夜夜骑| 亚洲无遮挡| 911香蕉视频| 影音先锋aV成人无码电影| 亚洲视频在线观看中文字幕 | 麻豆视频免费观看| 中文字幕无码Av在线看| 国产精品可站18| 亚洲观看黄色网| 中文字幕一区二区三区四虎在线 | 亚洲天堂在线观看免费视频| 天天肏屄| 人妻无码91| AV无码免费| 久久婷婷国产| 日日夜夜草| 懂色av懂色av粉嫩av无码| 国产视频导航| 毛片a级| 91免费在线看| 无码三级在线免费观看| 免费毛片在线| 免费草逼视频| 亚洲女人天堂AV| 999国产视频| 精品无码一区二区三区四区| 人人摸人人看人人| 中文在线高清字幕| 欧美一区二区丁香五月天激情 | 狼友视频在线免费观看| 久久毛片人妻| 亚洲性无码| 色五月婷婷中文字幕| 国产精品黑人ThePorn| 久久久久久综合| 另类老妇奶性BBWBBw| 久久无码专区| 国内免费av| 日韩无码123区| 特级西西444www大精品| 国产成人在线视频| 男人V天堂| 欧美精品在线播放| 国产精品白浆| 三级片一区二区| 久久人妻无码中文字幕系列| 午夜综合网| 精品三级| 黄色片成人| 久久久久久精| 波多野吉衣视频| 国产又粗又猛又爽又黄91精品| 色婷婷18| 99视频+国产日韩欧美| 日韩精品成人无码免费| 操逼基地| 日韩三级片无码| 最全av在线| 手机AV在线播放| 九色自拍| 操逼网站在线看| 人妻av在线| 色呦呦视频| 亚洲综合另类| 天天综合网久久| 亚洲日本高清| 熟妇人妻久久中文字幕| 精品日韩一区二区三区| 芳芳的骚逼| 9l视频自拍九色9l视频成人| 亚洲AV无码专区一级婬片毛片 | 91香蕉| 韩国免费一级a一片在线播放| 国产AA| 69自拍视频| 99自拍视频| 国产日韩一区二区三免费高清| 青青草免费在线| 精品乱子伦一区二区在线播放| 国产操比视频| 国产精品无码乱伦| 国产婷婷色一区二区三区| 亚洲偷拍中文| 亚洲无码在线视频播放| www日韩无码| 欧美日韩精品在线视频| 自拍偷拍免费| 天天操天天操天天操天天操| 国精产品秘一区二区| 偷拍亚洲天堂| 色婷婷黄色| 围产精品久久久久久久| 97男人的天堂| 婷婷狠狠干| 西西4444www无码精品| 欧美中文字幕在线播放| 97这里只有精品| 精品成人免费视频| 欧美熟妇性爱视频| 亚洲最大视频| 在线v片| 国产欧美综合一区二区三区| 日本成人不卡| 日本激情网站| 午夜试看120秒体验区的特点| 日韩A级毛片| 91国内产香蕉| 91人人爽| 欧美成人福利在线观看| 久草福利| 一本色道久久综合无码| 日韩无任何视频在线观看| 国产尤物视频| 伊人日韩| 亚洲综合另类| 国产思思99re99在线观看| 国产TS丝袜人妖系列视频| 草草草视频| 男女啪啪免费| 欧美熟妇BBB搡BBB| 欧美性猛交一区二区三区| 亚洲最大福利视频| 麻豆91精品91久久久停运原因| 毛片小说| 无码中文综合成熟精品AV电影| 91大鸡巴| 特写毛茸茸BBwBBwBBw| 伊人成人在线观看| 日韩中文无码字幕| VA电影| 爱爱视频天天操| 日韩欧美中文在线| 久草高清视频| 黄色大片在线免费观看| 动漫啪啪视频| 日韩精品91| 日韩黄色视频在线观看| 天堂中文字幕| 91无码影院| 亚洲无码自拍| 精品一区二区三区在线观看 | 久艹| 影音av资源| 国产精品91久久久| 亚洲久久久久久| 2025精品偷拍视频| 免费av片| TokyoKot大交乱无码| 日韩欧美一区二区在线观看| 国产激情视频在线观看| 日韩综合在线观看| 亚洲秘AV无码一区二区qq群 | 欧洲一级片| 成年人视频网| 亚洲天堂在线视频| 日本国产在线观看| 婷婷激情四射| 狠狠狠狠狠狠狠狠| 男女日日批黄色三级| 在线日韩av| 波多野结衣网址| 成人网大香蕉| 污视频在线| 日韩无码首页| 一区二区三区www污污污网站| 亚洲免费网站| 3344在线观看免费下载视频| 亚洲激情欧美激情| 午夜福利码一区二区| 婷婷久久在线| 粉嫩小泬BBBB免费看| 老熟女露脸25分钟91秒| 无码五区| 精品免费囯产| 成人肏逼视频| 欧美成人一级片| 国产精品9999久久久久仙踪林 | 亚洲色图成人网| 国产乱子伦一区二区三| 日韩三区| 囯产精品久久久久久久| 欧美69成人| A片在线免费观看| 国产乱人| 欧美AAAAAAAAAA特级| 国产熟妇码视频黑料| 黄色特级毛片| 无码精品黑人| 日韩一级A片| 欧美a级视频| 日韩中文字幕国产| 日韩本色一区| 亚洲无码一区二区三区| 婷婷丁香色| 国产成人宗合| 亚洲香蕉在线观看| 国产女人高潮毛片| 两根茎一起进去好爽A片在线观看| 波多野结衣成人在线| 国产高清无码一区| 91国产视频在线播放| 亚洲成人无码AV| AV色天堂| 一本色道久久综合熟妇人妻| 久久激情网| 91久久免费视频| 亚洲国产高清国产精品| 黄色片在线播放| 日韩a片在线观看| 日韩免费片| 国精品无码人妻一区二区三区免费| 内射极品美女| 久久免费视频,久久免费视频| 亚洲无吗在线播放| 91蜜桃视频| 亚洲高清无码免费观看| 午夜试看120秒体验区的特点| 国产精品免费久久影院| 操逼亚洲| 欧美AAA片| 人人看人人摸人人插| 激情毛片| 五月天伊人| 日韩无码av电影| 日韩vA| 伊人网成人| 青娱乐国产视频| 日本AⅤ| 国产黄色AV片| 久久无码一区| 日韩欧美一级视频| 北条麻妃中文字幕旡码| 国产精品秘入口18禁网站| 黄网免费在线观看| 中文在线字幕免费观| 一级一级a免一级a做免费线看内裤 | 翔田千里無碼破解| 中文字幕36页| 人妻精品一区二区在线| 欧美A片在线观看| 污视频在线看| 中文字幕日韩一级| 天天干天天在线观看| 在线免费看黄色| 北京熟妇槡BBBB槡BBBB| 安徽妇搡BBBB搡BBBB按摩| 大BBBw大BBBW另类| 北条麻妃无码视频| 一区二区高清视频| 热久久91| 超碰在线图片| 乱子伦一区二区三区视频在线观看 | 国产精品久久久91| 俺也操| 熟练中出-波多野结衣| 尿在小sao货里面好不好| 伦理无码| 天天色免费视频| 午夜九九| 午夜精东影业传媒在线观看| 日韩高清成人无码| 国产成人精品毛片| 无码秘蜜桃一区二区| 青青青视频在线| 国产又爽又黄免费网站在线| 亚洲无码中| 中文字幕无码精品| 日韩中文字幕| 黄片网站视频| 91视频免费播放| 中文字幕免费MV第一季歌词| 中文字幕亚洲人妻| 亚洲无码AV麻豆| 国产A区| 日韩激情无码一区二区| 久久久久久久久久成人永久免费视频 | 黑人精品XXX一区一二区| 欧美美女日逼视频| 亚洲天堂久久| 特级毛片片A片AAAAAA| 国产伦子伦一级A片在线| 亚洲AV无码成人片在线| 337P大胆粉嫩噜噜噜| 成人黄色大片| 久久久999久久久999精神| 婷婷深爱| 免费无码蜜臀在线观看| 国产精品7777| 黄片高清无码| 成人久久久久一级大黄毛片中国| 操b视频在线播放| 三级国产网站| 三级在线视频| 欧美一区二区三区免费| 国产激情在线观看视频| 国外操逼视频| 佳佳女王footjob超级爽| 成人毛片在线观看| 欧美精品第一页| 国产高清中文字幕| 乱伦小说五月天| 大香蕉伊人成人网| 97人妻精品| 日本无码专区| 亚洲欧洲免费| 久热只有精品| 97人妻精品黄网站| 日韩无码视频一区| 免费一级黄色视频| 18禁网站免费| 欧美黄色三级视频| 骚片网站| 日韩黄色片在线观看| 777偷窥盗摄00000| av久| 91视频在线看| 国产成人99久久亚洲综合精品| 天天操人妻| 中文电视剧字幕在线播放免费视频| 亚洲国产成人在线视频| 91吴梦梦无码一区二区| 午夜视频免费在线观看| 蜜臀av一区二区三区| 国产精品视频免费观看| 国产美女操逼| 国产又粗又长的视频| 国产成人一级片| 先锋影音成人| 高清亚洲| AAA三级视频| AV免费网站| 高清国产mv在线观看| 久久久穴| 无码中文字幕在线视频| 天天天天操| 欧美日韩成人网站| 91在线无码精品秘软件| 亚洲国产一区二区在线| 亚洲女人天堂| 婷婷一区二区| 成人免看一级a一片A片| 国产多人搡BBBB槡BBBB| 一区二区成人视频| 精品一区二区三区四区五区六区七区八区九区 | 91亚洲高清| 先锋资源av在线| 中文字幕第11页| 9久久精品| 国产黄色片在线免费观看| 无码A级| 西西4444www无码精品| 无码三级| 黄色国产| 国产精品久久久精品| 在线免费观看一区| 少妇搡BBBB搡BBB搡造水多| 色婷婷国产精品综合在线观看| 中文字幕视频免费| 中文字幕36页| 影音先锋黄色资源| 久久亚洲AV无码午夜麻豆| 亚洲韩国中文字幕| 日韩一区二区在线观看| 在线无码一区| 黄色一级片视频| 狼友精品| 色99视频| 成人日批视频| 五月天婷婷久久| 日本狠狠干| 亚洲第一中文字幕| 中文字幕国产一区| 最新中文| 亚洲Av无码午夜国产精品色软件| 夜夜躁狠狠躁| 国精产品一区二区三区在线观看 | av亚洲波多野结衣白嫩水多波 | v天堂| 久久午夜无码鲁丝片主演是谁| 色色视频网| 欧美日韩在线观看中文字幕| 日本A∨在线| 在线观看中文字幕亚洲| 国产乱子伦-区二区| AAA三级视频| 一级二级无码| 中文字幕永久在线5| 黑巨茎大战欧美白妞| 日韩视频一区| 山东熟妇搡BBBB搡BBBB| 亚洲天堂一区| 加勒比无码在线| h无码| 亚洲视频免费完整版在线播放| 亚洲成人网在线| 精品国产黄色| 午夜AAA| av五月| 日韩精品久久久久久久酒店| 亚洲春色一区二区三区| www男人天堂| 精品免费在线观看| 成人av中文字幕| 无码国产一区二区三区四区五区| 澳门四虎影院| 西西337| 成人视频黄片| 高清免费在线中文Av| 黄色不卡视频| 欧洲天堂在线视频网站| 一区二区三区在线看| 天天操天天日天天干| 在线免费看黄色| 国产骚逼| 欧美成人视频| 一级操逼黄色视频| 无码人妻日韩精品一区二区三| 网站色色免费看| 综合天堂网| 午夜精品久久久久久久久久久久 | 久久一区| 成人小说视频在线社区| 麻豆天美传媒AV果冻传媒| 色综合99| 91人妻无码| 日韩AAA| 人妻在线你懂的| 亚洲欧美美国产| 水果派成人播放无码| 豆花成人在线| 一本色道久久88综合无码| 久久香蕉电影| 亚洲无遮挡| 免费爱爱网站| 免费A级黄片| 77777精品成人免费A片| 国产剧情一区二区三区| 久热视频在线| 午夜国产在线视频| 国产视频精品一区二区三区| 1000部毛片A片免费视频| 91在线无码精品在线看| 三级片视频网址| 北条麻妃在线不卡| 北条麻妃91视频| 有码视频在线观看| 逼逼影院| 一级a免一级a做免费线看内祥| 久久久久无码国产精品不卡| 热久久免费| 久久久久无码| 国产免费麻豆| 黄色在线免费观看| 爱爱打炮影院| 国产欧美一区二区三区视频 | www.cao| 水果派成人播放无码| 久操精品视频| 精品中文视频| 国精产品一区一区三区有限公司杨 | 高清无码在线看| 91成人在线| 北条麻妃在线播放一区| 欧美日韩午夜福利视频| 大香蕉伊人成人网| av在线观看网站| 亚洲成人高清在线| 免费黄色在线视频| 天天色天天干天天日| 无码视频网站| BBWBBw嫩| 超碰在线网| 狼友免费视频| 欧美成人精品三级网站| 亚洲尤物在线| 操人视频在线观看| 久操无码| 白浆av| 99中文字幕| 俺去搞| www.伊人大香蕉| 一级片在线免费看| 91丨PORN丨国产| 国产高清久久| 91精品婷婷国产综合久久| 日韩精品视频在线免费观看| 国产一区无码| 91一区二区三区| 成人av影院| 操逼大全| 亚洲色情视频| 午夜无码在线观看视频| 开心五月婷| 自拍三级| 日韩欧美人妻| wwwsesese| 精品国产乱码一区二区| 国产做受精品网站在线观看| 亚洲va综合va国产va中文| 黄色激情av| 大香蕉青娱乐| 青娱乐Av| 欧美婷婷五月天| 人人妻人人澡人人爽人人| 日韩不卡| 欧美大胆视频| 中文亚洲视频| 成人一级a片| 亚洲午夜久久久久久久久红桃 | 九色视频在线观看| 国内无码精品| 99re这里只有精品6| 欧美日韩中文字幕在线视频| JLZZJLZZ亚洲女人| 久久99热这里只频精品6学生| 无码一区二区三区四区五区| 狼人香蕉在线视频| 天天草天天日| 日韩中文字幕在线人成网站| 欧美精品一级片| 蜜桃91视频| 91精品人妻一区二区| 啊啊啊啊国产| 91嫖妓站街按店老熟女| 91激情网| 婷婷午夜精品久久久久久性色AV| 少妇大战黑人46厘米| 国产精品秘入口18禁网站| 综合自拍偷拍| 国产黄色视频免费在线观看| 无码a片| 在线黄色网| 亚洲国产一区二区在线| 免费爱爱网站| 日韩欧美成人在线视频| 北条麻妃无码精品AV| 亚洲免费高清| 国产精品特级毛片| 在线播放91灌醉迷J高跟美女 | 日本午夜三级视频| 操逼黄视频| 在线国产中文字幕| 欧美亚洲天堂网| 97伊人| 天堂成人AV| 亚洲男女网站| 一级特黄AA片| 国产精品视频免费| 国产在线观看97| 无码视频中文字幕| 999一区二区三区| 中文无码熟妇人妻AV在线| 日韩国产在线观看| 中文字幕资源站| 91插插插插| 欧美性爱xxxx| 久久丝袜| 欧洲三级片网站| 久久久久久久9999| 青青草91视频| 亚洲精品在线视频| 九九九在线观看视频| 亚洲深夜福利| www.狠狠爱| 91视频一区二区三区| 精品黄色毛片| 日韩无码AV中文字幕| 三级av无码| 欧美成人高清| 操操操操操操操操逼| 黄色电影视频在线| 最新中文字幕免费MV第一季歌词| 狼友视频在线| 国产一级a毛一级a做免费的视频 | 国产在线拍揄自揄拍无码男男 | 91麻豆国产| 91丨九色丨蝌蚪丨肥女| 人人澡人人摸| 2025av天堂网| 亚洲热热| 国产精品无码成人AV电影| 国产videos| 成人在线国产| 午夜性爽视频男人的天堂| 91嫖妓站街埯店老熟女| 99精品色| 午夜成人精品视频| 亚洲精品三级在线观看| 欧美性交网| 91高潮| 亚洲AV无码一区二区三竹菊| 成年人黄色视频| 成人精品三级麻豆| 人妻97| 日本少妇性爱视频| 亚洲网站在线播放| 亚洲中文字幕在线免费观看视频 | 最近中文字幕mv第三季歌词| av一区二区三区| 久久福利电影| 国产视频第一页| 超碰免费在线观看| 91人妻一区二区三区| 无码精品成人观看A片| 无码人妻丰满熟妇区毛片视频| 欧美黄片免费视频| 青青草乱伦视频| 西西444WWW无码大胆| 黄色视频在线免费观看高清视频 | 高清无码不卡在线观看| 日本少妇高潮| 中文字幕黑人无码| 一级黄色片网站| 大香蕉操| 欧美美女日逼视频| 国产精品无码激情| 五月婷婷一区| 国外亚洲成AV人片在线观看| 另类老妇奶性BBWBBwBBw| 国产综合AV| 在线观看三级网址| 狠狠躁日日躁夜夜躁A片男男视频 精品无码一区二区三区蜜桃李宗瑞 | 日韩一级在线播放| 三级片日本在线| 男女av免费观看| 亚洲AV无码成人精品区欧洲| 少妇性视频| 精品码产区一区二亚洲国产| av高清| www.wuma| 成年人在线播放| 爱搞逼综合网| 先锋影音男人资源站| 边吃边摸| 亚洲视频在线免费观看| 国产黄色片在线播放| 午夜福利资源| 欧美精品午夜福利无码| 高清无码在线视频观看| 黄色性爱小说| 国产午夜福利视频在线观看| 欧美性爱天天操| 黄色在线网站| 高潮毛片| 无码黄| 熟妇人妻久久中文字幕|