前端监听SDK(上报埋点数据)

news2024/11/15 23:29:05

1、使用方式

<head>
  <script>
    window.pineapple || (pineapple = {});
    pineapple.param = {
      "src": "http://127.0.0.1:3001/pa.gif",
      "token": "dsadasd2323dsad23dsada",
    };
  </script>
  <script src="js/pineapple.js"></script>
</head>

2、上报SDK封装

使用image的形式进行接口数据传输

(function(window) {
	'use strict';
	
	/**
	 * https://developer.mozilla.org/zh-CN/docs/Web/API/Window/performance
	 */
	var performance = window.performance || window.webkitPerformance || window.msPerformance || window.mozPerformance || {};
	performance.now = (function() {
        return performance.now    ||
        performance.webkitNow     ||
        performance.msNow         ||
        performance.oNow          ||
        performance.mozNow        ||
        function() { return new Date().getTime(); };
    })();
    
	/**
	 * 默认属性
	 */
	var defaults = {
		performance: performance,   // performance对象
		ajaxs: [],                  //ajax监控
		//可自定义的参数
		param: {
			rate: 0.5,              //随机采样率
			src: 'http://127.0.0.1:3001/pa.gif'                 //请求发送数据
		}
	};
	
	if(window.pineapple.param) {
		for(var key in window.pineapple.param) {
			defaults.param[key] = window.pineapple.param[key];
		}
	}
	var pineapple = defaults;
	var firstScreenHeight = window.innerHeight;         //第一屏高度
	var doc = window.document;
    
    // 定义的错误类型码
    var ERROR_RUNTIME = 1
    var ERROR_SCRIPT = 2
    var ERROR_STYLE = 3
    var ERROR_IMAGE = 4
    var ERROR_AUDIO = 5
    var ERROR_VIDEO = 6
    var ERROR_PROMISE = 7
    var LOAD_ERROR_TYPE = {
        SCRIPT: ERROR_SCRIPT,
        LINK: ERROR_STYLE,
        IMG: ERROR_IMAGE,
        AUDIO: ERROR_AUDIO,
        VIDEO: ERROR_VIDEO
    };
    /**
     * 上报错误
     * @param  {Object} errorLog    错误日志
     */
    function handleError (errorLog) {
        // console.log(errorLog);
        pineapple.sendError(errorLog);
    }
	/**
	 * 监控资源异常
     * https://github.com/BetterJS/badjs-report
	 */
    window.addEventListener("error", function(event) {
        var errorTarget = event.target
        // 过滤 target 为 window 的异常
        if (errorTarget !== window && errorTarget.nodeName && LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()]) {
            handleError(formatLoadError(errorTarget))
        } else {
            handleError(formatRuntimerError(event.message, event.filename, event.lineno, event.colno, event.error))
        }
    }, true);
    /**
	 * 监控未处理的Promise错误
     * 当 Promise 被 reject 且没有 reject 处理器时触发
	 */
    window.addEventListener("unhandledrejection", function(event) {
        // console.log('Unhandled Rejection at:', event.promise, 'reason:', event.reason);
        handleError({
            type: ERROR_PROMISE,
            desc: event.reason,
            stack: 'no stack'
        });
    }, true);
    /**
     * 生成 laod 错误日志
     * 需要加载资源的元素
     * @param  {Object} errorTarget
     */
    function formatLoadError (errorTarget) {
        return {
            type: LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()],
            desc: errorTarget.baseURI + '@' + (errorTarget.src || errorTarget.href),
            stack: 'no stack'
        };
    }
    /**
     * 生成 runtime 错误日志
     * @param {String}  message      错误信息
     * @param {String}  filename     出错文件的URL
     * @param {Long}    lineno       出错代码的行号
     * @param {Long}    colno        出错代码的列号
     * @param {Object}  error        错误信息Object
     * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error
     */
    function formatRuntimerError (message, filename, lineno, colno, error) {
        return {
            type: ERROR_RUNTIME,
            desc: message + ' at ' + filename + ':' + lineno + ':' + colno,
            stack: error && error.stack ? error.stack.replace(/\n/gi, "") : 'no stack'      // IE <9, has no error stack
        };
    }
	
	/**
	 * ajax监控
	 * https://github.com/HubSpot/pace
	 */
	var _XMLHttpRequest = window.XMLHttpRequest;        //保存原生的XMLHttpRequest
	//覆盖XMLHttpRequest
	window.XMLHttpRequest = function(flags) {
	    var req;
	    req = new _XMLHttpRequest(flags);   //调用原生的XMLHttpRequest
	    monitorXHR(req);                    //埋入我们的“间谍”
	    return req;
	};
	var monitorXHR = function(req) {
		req.ajax = {};
		//var _change = req.onreadystatechange;
		req.addEventListener('readystatechange', function() {
			if(this.readyState == 4) {
				req.ajax.end = pineapple.now();     //埋点

				if ((req.status >= 200 && req.status < 300) || req.status == 304 ) {    //请求成功
					req.ajax.endBytes = _kb(req.responseText.length * 2);               //KB
					//console.log('响应数据:'+ req.ajax.endBytes);//响应数据大小
				}else {     //请求失败
					req.ajax.endBytes = 0;
				}
				req.ajax.interval = req.ajax.end - req.ajax.start;
				pineapple.ajaxs.push(req.ajax);
				//console.log('ajax响应时间:'+req.ajax.interval);
			}
		}, false);
		
		// “间谍”又对open方法埋入了间谍
		var _open = req.open;
		req.open = function(type, url, async) {
			req.ajax.type = type;       //埋点
			req.ajax.url = url;         //埋点
	    	return _open.apply(req, arguments);
		};
		
		var _send = req.send;
		req.send = function(data) {
			req.ajax.start = pineapple.now();   //埋点
			var bytes = 0;                      //发送数据大小
			if(data) {
				req.ajax.startBytes = _kb(JSON.stringify(data).length * 2 );
			}
	    	return _send.apply(req, arguments);
		};
	};
	
	/**
	 * 计算KB值
	 * http://stackoverflow.com/questions/1248302/javascript-object-size
	 */
	function _kb(bytes) {
		return _rounded(bytes / 1024, 2);       //四舍五入2位小数
	}

	/**
	 * 四舍五入
	 */
	function _rounded(number, decimal) {
		return parseFloat(number.toFixed(decimal));
	}
	
	/**
	 * 给所有在首屏的图片绑定load事件,计算载入时间
	 * TODO 忽略了异步加载
	 * CSS背景图 是显示的在param参数中设置backgroundImages图片路径数组加载
	 */
	var imgLoadTime = 0;
	function _setCurrent() {
		var current = Date.now();
	    current > imgLoadTime && (imgLoadTime = current);
	}
	doc.addEventListener('DOMContentLoaded', function() {
	    var imgs = doc.querySelectorAll('img');
	    imgs = [].slice.call(doc.querySelectorAll('img'));
	    if(imgs) {
	    	imgs.forEach(function(img) {
		    	if(img.getBoundingClientRect().top > firstScreenHeight) {
		    		return;
		    	}
	//	    	var image = new Image();
	//      	image.src = img.getAttribute('src');
		    	if(img.complete) {
		    		_setCurrent();
		    	}
		    	//绑定载入时间
		    	img.addEventListener('load', function() {
		    		_setCurrent();
		    	}, false);
		    });
	    }
	    
	    //在CSS中设置了BackgroundImage背景
	    if(pineapple.param.backgroundImages) {
	    	pineapple.param.backgroundImages.forEach(function(url) {
	    		var image = new Image();
	    		image.src = url;
	    		if(image.complete) {
		    		_setCurrent();
		    	}
	    		image.onload = function() {
	    			_setCurrent();
	    		};
	    	});
	    }
	}, false);

	/**
	 * 递归的将数字四舍五入小数点后两位
	 */
	function handleNumber(obj) {
		var type = typeof obj;
		if(type === "object" && type !== null) {
			for(var key in obj) {
				obj[key] = handleNumber(obj[key]);
			}
		}
		if(type === "number") {
			return _rounded(obj, 2);
		}
		return obj;
	}

	window.addEventListener('load', function() {
		setTimeout(function() {
			var time = pineapple.getTimes();
            var data = handleNumber({ ajaxs:pineapple.ajaxs, dpi:pineapple.dpi(), time:time, network:pineapple.network() });
            console.log("data", data);
			pineapple.send(data);
		}, 500);
	});
	
	/**
	 * 打印特性 key:value格式
	 */
	pineapple.print = function(obj, left, right, filter) {
		var list = [], left = left || '', right = right || '';
		for(var key in obj) {
			if(filter) {
				if(filter(obj[key]))
					list.push(left + key + ':' + obj[key] + right);
			}else {
				list.push(left + key + ':' + obj[key] + right);
			}
		}
		return list;
	};
	
	/**
	 * 请求时间统计
	 * 需在window.onload中调用
	 * https://github.com/addyosmani/timing.js
	 */
	pineapple.getTimes = function() {
		var timing = performance.timing;
		if (timing === undefined) {
			return false;
		}
		var api = {};
		//存在timing对象
		if (timing) {
			// All times are relative times to the start time within the
			// 白屏时间,也就是开始解析DOM耗时
			var firstPaint = 0;

			// Chrome chrome.loadTimes()已废弃
			// if (window.chrome && window.chrome.loadTimes) {
			// 	var chromeLoad = window.chrome.loadTimes();
			// 	// Convert to ms
			// 	firstPaint = chromeLoad.firstPaintTime * 1000;
			// 	api.firstPaintTime = firstPaint - (chromeLoad.startLoadTime * 1000);
			// }
			// IE
			if (typeof timing.msFirstPaint === 'number') {
				firstPaint = timing.msFirstPaint;
				api.firstPaintTime = firstPaint - timing.fetchStart;
			}
			else {
				api.firstPaintTime = currentTime - timing.fetchStart;
			}
			// Firefox
			// This will use the first times after MozAfterPaint fires
			//else if (window.performance.timing.fetchStart && typeof InstallTrigger !== 'undefined') {
			//    api.firstPaint = window.performance.timing.fetchStart;
			//    api.firstPaintTime = mozFirstPaintTime - window.performance.timing.fetchStart;
			//}

			/**
			 * http://javascript.ruanyifeng.com/bom/performance.html
			 * 加载总时间
			 * 这几乎代表了用户等待页面可用的时间
			 * loadEventEnd(加载结束)-navigationStart(导航开始)
			 */
			api.loadTime = timing.loadEventEnd - timing.navigationStart;
			
			/**
			 * Unload事件耗时
			 */
			api.unloadEventTime = timing.unloadEventEnd - timing.unloadEventStart;
			
			/**
			 * 执行 onload 回调函数的时间
			 * 是否太多不必要的操作都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么?
			 */
			api.loadEventTime = timing.loadEventEnd - timing.loadEventStart;
			
			/**
			 * 用户可操作时间
			 */
			api.domReadyTime = timing.domContentLoadedEventEnd - timing.fetchStart;

			/**
			 * 首屏时间
			 * 用户在没有滚动时候看到的内容渲染完成并且可以交互的时间
			 * 记录载入时间最长的图片
			 */
			if(imgLoadTime == 0) {
				api.firstScreen = api.domReadyTime;
			}else {
				api.firstScreen = imgLoadTime - timing.fetchStart;
			}

			/**
			 * 解析 DOM 树结构的时间
			 * 期间要加载内嵌资源
			 * 反省下你的 DOM 树嵌套是不是太多了
			 */
			api.parseDomTime = timing.domComplete - timing.domInteractive;
			
			/**
			 * 请求完毕至DOM加载耗时
			 */
			api.initDomTreeTime = timing.domInteractive - timing.responseEnd;
			
			/**
			 * 准备新页面时间耗时
			 */
			api.readyStart = timing.fetchStart - timing.navigationStart;

			/**
			 * 重定向的时间
			 * 拒绝重定向!比如,http://example.com/ 就不该写成 http://example.com
			 */
			api.redirectTime = timing.redirectEnd - timing.redirectStart;

			/**
			 * DNS缓存耗时
			 */
			api.appcacheTime = timing.domainLookupStart - timing.fetchStart;

			/**
			 * DNS查询耗时
			 * DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长?
			 * 可使用 HTML5 Prefetch 预查询 DNS ,见:[HTML5 prefetch](http://segmentfault.com/a/1190000000633364)
			 */
			api.lookupDomainTime = timing.domainLookupEnd - timing.domainLookupStart;

            /**
			 * SSL连接耗时
			 */
			var sslTime = timing.secureConnectionStart;
            api.connectSslTime = sslTime > 0 ? (timing.connectEnd - sslTime) : 0;
 
			/**
			 * TCP连接耗时
			 */
			api.connectTime = timing.connectEnd - timing.connectStart;

			/**
			 * 内容加载完成的时间
			 * 页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么?
			 */
			api.requestTime = timing.responseEnd - timing.requestStart;
			
			/**
			 * 请求文档
			 * 开始请求文档到开始接收文档
			 */
			api.requestDocumentTime = timing.responseStart - timing.requestStart;
			
			/**
			 * 接收文档(内容传输耗时)
			 * 开始接收文档到文档接收完成
			 */
			api.responseDocumentTime = timing.responseEnd - timing.responseStart;

			/**
			 * 读取页面第一个字节的时间
			 * 这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么?
			 * TTFB 即 Time To First Byte 的意思
			 * 维基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte
			 */
			api.TTFB = timing.responseStart - timing.fetchStart;
		}

		return api;
	};
	
	/**
	 * 与performance中的不同,仅仅是做时间间隔记录
	 * https://github.com/nicjansma/usertiming.js
	 */
	var marks = {};
	pineapple.mark = function(markName) {
		var now = performance.now();
		marks[markName] = {
			startTime: Date.now(),
			start: now,
			duration: 0
		};
	};
	
	/**
	 * 计算两个时间段之间的时间间隔
	 */
	pineapple.measure = function(startName, endName) {
		var start = 0, end = 0;
		if(startName in marks) {
			start = marks[startName].start;
		}
		if(endName in marks) {
			end = marks[endName].start;
		}
		return {
			startTime: Date.now(),
			start: start,
			end: end,
			duration: (end - start)
		};
	};
	
	/**
	 * 标记时间
	 * Date.now() 会受系统程序执行阻塞的影响不同
	 * performance.now() 的时间是以恒定速率递增的,不受系统时间的影响(系统时间可被人为或软件调整)
	 */
	pineapple.now = function() {
		return performance.now();
	};
	
	/**
	 * 网络状态
	 * https://github.com/daniellmb/downlinkMax
	 * http://stackoverflow.com/questions/5529718/how-to-detect-internet-speed-in-javascript
	 */
	pineapple.network = function() {
		//2.2--4.3安卓机才可使用
        var connection = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection,
            effectiveType = connection.effectiveType;
        if(effectiveType) {
            return {bandwidth: 0, type: effectiveType.toUpperCase()};
        }
		var types = "Unknown Ethernet WIFI 2G 3G 4G".split(" ");
        var info = {bandwidth: 0, type: ""};
		if(connection && connection.type) {
			info.type = types[connection.type];
		}
		return info;
	};
	
	/**
	 * 分辨率
	 */
	pineapple.dpi = function() {
		return {width:window.screen.width, height:window.screen.height};
	};

	/**
	 * 组装变量
	 * https://github.com/appsignal/appsignal-frontend-monitoring
	 */
	function _paramify(obj) {
		obj.token = pineapple.param.token;
		return JSON.stringify(obj);
	}
	
	/**
	 * 推送统计信息
	 */
	pineapple.send = function(data) {
		var ts = new Date().getTime().toString();
		//采集率
		if(pineapple.param.rate > Math.random(0, 1)) {
			var img = new Image(0, 0);
    		img.src = pineapple.param.src +"?data=" + _paramify(data) + "&ts=" + ts;
		}
    };
    
    /**
	 * 推送错误信息
	 */
	pineapple.sendError = function(data) {
		var ts = new Date().getTime().toString();
		var img = new Image(0, 0);
    	img.src = pineapple.param.src +"?error=" + _paramify(data) + "&ts=" + ts;
	};
	
	var currentTime = Date.now();       //这个脚本执行完后的时间 计算白屏时间
	window.pineapple = pineapple;
})(this);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/958221.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

sql各种注入案例

目录 1.报错注入七大常用函数 1)ST_LatFromGeoHash (mysql>5.7.x) 2)ST_LongFromGeoHash &#xff08;mysql>5.7.x&#xff09; 3)GTID (MySQL > 5.6.X - 显错<200) 3.1 GTID 3.2 函数详解 3.3 注入过程( payload ) 4)ST_Pointfromgeohash (mysql>5.…

如何截取视频中的一段视频?分享几种视频分割方法

当处理长视频时&#xff0c;视频分割可以使您更加高效。如果您只需要处理其中的一部分&#xff0c;而不是整个视频&#xff0c;那么分割视频可以使您更容易找到需要处理的部分。而且&#xff0c;分割视频还可以使您更容易在不同的项目之间重复使用视频片段。教大家几种简单的视…

前端面试中Vue的有经典面试题一

1. 谈谈你对MVVM开发模式的理解 MVVM分为Model、View、ViewModel三者。 Model&#xff1a;代表数据模型&#xff0c;数据和业务逻辑都在Model层中定义&#xff1b; View&#xff1a;代表UI视图&#xff0c;负责数据的展示&#xff1b; ViewModel&#xff1a;负责监听Model中…

Argument of type {****} is not assignable to parameter of type ‘never‘.ts(2345)

在日常开发中配置eslintTypescript之后&#xff0c;会出先各种校验报错提示&#xff0c;今天在开发过程中遇到ts2345报错&#xff0c;出错场景发生在数组push一个Object对象报错。 const obj { title: , children: [] }; const currentObj obj.children; 其实就是obj.chilr…

C语言每日一练---Day(14)

本专栏为c语言练习专栏&#xff0c;适合刚刚学完c语言的初学者。本专栏每天会不定时更新&#xff0c;通过每天练习&#xff0c;进一步对c语言的重难点知识进行更深入的学习。 今日练习题关键字&#xff1a;统计每个月兔子的总数 数列的和 &#x1f493;博主csdn个人主页&#x…

链表模拟栈

定义节点 class Node {var num: Int _var next: Node _def this(num: Int) {thisthis.num num}override def toString: String s"num[${this.num}]" }定义方法 class LinkStack {private var head new Node(0)def getHead: Node head//判断是否为空def isEmp…

每日一题 1110删点成林

题目 给出二叉树的根节点 root&#xff0c;树上每个节点都有一个不同的值。 如果节点值在 to_delete 中出现&#xff0c;我们就把该节点从树上删去&#xff0c;最后得到一个森林&#xff08;一些不相交的树构成的集合&#xff09;。 返回森林中的每棵树。你可以按任意顺序组…

Pandas+Pyecharts | 2023软科中国大学排名分析可视化

文章目录 &#x1f3f3;️‍&#x1f308; 1. 导入模块&#x1f3f3;️‍&#x1f308; 2. Pandas数据处理2.1 读取数据2.2 数据信息 &#x1f3f3;️‍&#x1f308; 3. Pyecharts数据可视化3.1 2023中国大学综合排名TOP303.2 2023中国大学各类型占比3.3 2023中国各省地区大学…

Redis 复制(replica)

1. 是什么 1.1 官网地址 https://redis.io/docs/management/replication/ 1.2 一句话 1. 就是主从复制&#xff0c;master以写为主&#xff0c;slave以读为主 2. 当master数据变化的时候&#xff0c;自动将新的数据异步同步到其它slave数据库 2. 能干嘛 1. 读写分离 2. 容灾…

准备HarmonyOS开发环境

引言 在开始 HarmonyOS 开发之前&#xff0c;需要准备好开发环境。本章将详细指导你如何安装 HarmonyOS SDK、配置开发环境、创建 HarmonyOS 项目。 目录 安装 HarmonyOS SDK 配置开发环境 创建 HarmonyOS 项目 总结 1. 安装 HarmonyOS SDK HarmonyOS SDK 是开发 Harmo…

共享办公空间的SWOT分析:

S&#xff08;优势&#xff09;&#xff1a; 灵活性和多样性&#xff1a;共享办公空间通常提供多种套餐和服务&#xff0c;适合不同需求和预算的初创企业和个人。 资源共享和合作&#xff1a;共享办公空间提供了与其他企业家、创新者和专业人士交流和合作的机会&#xff0c;可…

为 LVGL 添加截图/截屏功能(lv_100ask_screenshot)

本文内容选自百问网&#xff0c;完整的演示视频观看&#xff1a; https://www.bilibili.com/video/BV18r4y1X7MJ 前言 lv_100ask_screenshot 是一个基于 lvgl 的屏幕截图工具。 lv_100ask_screenshot 特性&#xff1a; 可以将LVGL的屏幕对象(全屏)保存为图片文件&#xff1…

Deepnote:为什么我停止使用 Jupyter Notebook

Jupyter 笔记本已经成为必不可少多年来用于众多数据科学工作流程的工具。其中包括执行数据挖掘、分析、处理、建模以及在每个数据科学项目的生命周期中执行的一般日常实验任务。 Jupyter(作者提供的图片) 尽管它很受欢迎,但许多数据科学家也指出了它的众多缺点,例如这里和

ThingsKit物联网平台告警中心之告警联系人

告警联系人是指接收告警信息的人&#xff0c;产生告警后&#xff0c;会第一时间通知他。 新增 点击新增告警联系人按钮&#xff0c;填入相关信息&#xff0c;确认新增。 告警联系人参数参数说明联系人姓名 定义告警通知到的联系人名称必填支持输入的格式&#xff1a;中英文…

【LeetCode】84.柱状图中最大的矩形

题目 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 示例 1: 输入&#xff1a;heights [2,1,5,6,2,3] 输出&#xff1a;10 解释&#xff1a;最大的…

如何理解attention中的Q、K、V?

y直接用torch实现一个SelfAttention来说一说&#xff1a; 1、首先定义三哥线性变换&#xff0c;query&#xff0c;key以及value&#xff1a; class BertSelfAttention(nn.Module):self.query nn.Linear(config.hidden_size, self.all_head_size)#输入768&#xff0c;输出768…

2第一个Java程序

目录 1第一个Java代码 2类class 3运行Java文件 1第一个Java代码 public class Hello {public static void main(String[] args) {System.out.println("Hello, world!");} } 2类class public class Hello {public static void main(String[] args) {System.ou…

书箱扫描仪真神器,免拆书,AI助力自动识别翻页

平板扫描仪见多了&#xff0c;馈纸式扫描仪我们也介绍过了&#xff0c;但它们都不适合扫描书箱&#xff0c;如果您一定要用它们来完成这项任务&#xff0c;那将很费劲&#xff0c;首先&#xff0c;您得将书拆成一页一页的&#xff0c;然后再放进去扫&#xff0c;非常麻烦&#…

Docker从认识到实践再到底层原理(二-2)|Namespace+cgroups

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总 然后就是博主最近最花时间的一个专栏…

分库分表篇-2.3 springBoot 集成Mycat(1.6)

文章目录 前言一、springBoot 集成Mycat(1.6) 步骤&#xff1a;二、query_cache_size unknown 处理&#xff1a;总结&#xff1a; 前言 在springboot 项目中我们应该如何集成mycat 然后让其帮助我们进行数据的分库和分表处理呢。 一、springBoot 集成Mycat(1.6) 步骤&#xff…