一、用new Image().src作LOG统计的一个注意事项
在大型网站做很多用户行为分析、产品的策划方案基本上都是通过分析用户的访问等信息而做出的,LOG信息的统计准确性会直接影响到产品的设计开发(比如搜索结果的先后排名rank值的产生等)。目前最常用的一个写LOG的方法就是用JavaScript脚本在网页里 new Image().src = "http://xxx.com/log?msg="+ msg; 这种统计方法基本上不会干扰用户的正常操作,虽然有LOG丢失的可能,但只要用得好,还是一种非常好的LOG统计回收的方案。
但是这种LOG回收手段有一个非常隐秘的隐患,事情是这样被发现的。今年3月份百度搜索结果页面上线了Suggestion功能升级版,在LOG统计中突然发现上线后的LOG总量比上线前的少了很多(10%以上),表现出来的现象就是LOG丢失了,没有回收到服务器中来。
工程师立即做了详细排除,从各种因素上确定LOG的大量丢失跟网页上线Suggestion有关,我们马上对Suggestion脚本进行地毯式排查,逐行分析代码,结论是这个脚本没有阻拦 new Image().src 的发包请求(至少表面上是这样的),这段脚本放在一个脚本闭包中,没有影响到全局变量/方法,也没有屏蔽干扰HTTP请求的因素。
我们又做了一个线下试验,同样的代码环境,在线下的测试环境中用这种 new Image().src 的手段,总共向Server端发送了10000个LOG数据,也没有发生LOG丢失,试验的结果也没有发现LOG丢失的原因。
但是在线上的环境中发生的就是有LOG数据丢失,原因不明,所以只能紧急下线这个Suggestion升级版。在这个脚本下线之后,LOG统计数据马上回归到“原正常”状态。从这个现象来看,也从另一个角度说明上线的新脚本确实对LOG统计有影响,头疼呀......
愚者千虑,必有一得!最后方知这个问题的原因是浏览器的垃圾回收机制!
function c(q) {
var p=window.document.location.href,sQ='',sV='';
for(v in q){
switch (v){
case "title":sV=encodeURIComponent(q[v].replace(/<[^<>]+>/g,""));break;
case "url":sV=escape(q[v]);break;
default:sV=q[v]
}
sQ+=v+"="+sV+"&";
}
new Image().src = "http://s.baidu.com/w.gif?q=meizz&"+sQ+"path="+p+"&cid=9&t="+ new Date().getTime();
return true;
}
这个 new Image() 对象没有赋给任何变量,在这个函数执行结束时,浏览器的垃圾回收机制对这种“无主”的对象是毫不客气的回收的,而正是这种回收行为导致了这个HTTP请求(异步的)没有发出,从而造成了LOG数据的丢失。那为什么上线一个脚本就会造成大量的LOG丢失呢?因为一个大脚本的运行回产生大量的“垃圾”,浏览器垃圾回收也会相应地更频繁的启动,从而造成LOG数据丢失。找到原因之后对症下药,把这个 new Image() 对象赋给一个全局有变量常期持有即可,相应的代码如下:
var n = "log_"+ (newDate()).getTime();
var c = window[n] =newImage(); //把new Image()赋给一个全局变量长期持有
c.onload = (c.οnerrοr=function(){window[n] = null;});
c.src = "http://s.baidu.com/w.gif?q=meizz"+ xxxx;
c = null; //释放局部变量c
在这个统计代码上线之后,百度的搜索结果页面的LOG立即多出近10%,之后再上线其它的脚本也没有再出现LOG统计量的波动。
---------------------------------------------------------------------------------------------------------------------------------
局部变量下,测试连续发1000个log
IE6-7 发现log丢失,某些IE内核浏览器也发现log丢失
但在FF, chrome下未发现丢失
因此 保险的做法是用一个非局部变量持有
- var unique = (function () {
- var time= (new Date()).getTime()+'-', i=0;
- return function () {
- return time + (i++);
- }
- })();
- var imgLog = function (url) {
- var data = window['imgLogData'] || (window['imgLogData'] = {});
- var img = new Image();
- var uid = unique();
- img.onload = img.onerror = function () {//销毁一些对象
- img.onload = img.onerror = null;
- img = null;
- delete data[uid];
- }
- img.src = url + '&_uid=' + uid;
- };
- var unique = (function () {
- var time= (new Date()).getTime()+'-', i=0;
- return function () {
- return time + (i++);
- }
- })();
- var imgLog = function (url) {
- var data = window['imgLogData'] || (window['imgLogData'] = {});
- var img = new Image();
- var uid = unique();
- img.onload = img.onerror = function () {//销毁一些对象
- img.onload = img.onerror = null;
- img = null;
- delete data[uid];
- }
- img.src = url + '&_uid=' + uid;
- };
-
var unique = (
function () {
-
var time= (
new
Date()).getTime()+
'-', i=
0;
-
return
function () {
-
return time + (i++);
-
}
-
})();
-
-
var imgLog =
function (url) {
-
var data =
window[
'imgLogData'] || (
window[
'imgLogData'] = {});
-
var img =
new Image();
-
var uid = unique();
-
img.onload = img.onerror =
function () {
//销毁一些对象
-
img.onload = img.onerror =
null;
-
img =
null;
-
delete data[uid];
-
}
-
img.src = url +
'&_uid=' + uid;
-
};
二、页面img发送
记下了点击信息,如何发送出去呢?
考察了若干类似的系统,目前比较流行的方式似乎是将要发送的信息加到 URL 参数中,请求打点服务器上的一个非常小的空图片,这样,信息就将记录在打点服务器的日志(比如 apache 日志)中,之后再用专门的程序从日志中读取并分析相关信息。而且,对于打点服务器而言,由于只需要提供一个非常小的静态图片的访问,所以一台普通的服务器经过适当配置后也可以应对很高的访问量。
在 JavaScript 中,构造一个图片请求非常容易,比如:
1
2
|
var
img
=
new
Image
(
)
;
img
.
src
=
"http://xxx.xxx/img.gif?a=1&b=2...."
;
|
但这儿也有一些需要注意的地方。比如当点击在一个链接上时,页面很快就会发生跳转,而如果跳转之前 img 的请求还没有成功发出的话,这个请求就会丢失。当然,可以在发送 img 请求时设法先把页面跳转阻塞,img 请求发送完成后再继续跳转,但这种做法侵入性太高,有可能会危害用户体验,一般情况下不建议这么做。
一个减少这种情况发生的方案是,绑定 mousedown 事件而不是 click 事件,在鼠标按键按下之时即发送 img 请求。我们知道,只有在鼠标按键按下(mousedown)再弹起(mouseup)之后才是点击(click)事件(不考虑部分移动设备上的浏览器),而正常人在鼠标按下与弹起之间还有几十至一两百毫秒的时间差,有这额外的一点时间,虽然很少,但对于提高 img 请求发送的成功率来说已经很有帮助了。如果再优化一下打点服务器,让每次 img 请求响应都能尽可能快,因页面跳转发生的记录丢失就会降低很多(不过理论上还是会有一些)。
还有一个需要注意的是,在某些浏览器中,如果页面过于复杂,会频繁地触发浏览器的垃圾回收机制,如果刚好 img 又都是些没有引用的局部变量,有一些 img 就会被强制回收,请求也就发不出去了。关于这个问题,可以参考这个页面。比较好的做法是,先将 img 在某个全局变量中引用,等 img onload 或 onerror 时再将这个引用去掉。
有时也需要考虑,这一段监测用户鼠标点击行为的脚本应该放在页面的哪个位置。当然,放得越靠前越好,因为如果放得太靠后,并且网页加载速度不是很快的话,有可能监测脚本还没有加载用户就已经开始点击了,而这些点击记录显然也会丢失。但不少页面需要尽快向用户呈现首屏内容,对于这些页面来说,监测需求相对不是第一位的,监测脚本可以放到后面一些的位置。
另外,监测代码如果想写得尽量通用一些,不指定网页内容的根元素是哪一个的话(或者指定了根元素,但监测代码放在了根元素之前如 head 中),mousedown 事件基本上就要绑定到 document 对象上,这时,需要注意点击到网页滚动条上的记录是否要忽略的问题。
如何判断一个点击是否点在了页面的滚动条上是个有趣的问题,一个简单的办法是判断当前事件元素的父元素,如果这个元素的 tagName 直接就是“HTML”了,那说明当前点击到了滚动条上,不然的话,父元素应该是一个“DIV”、“BODY”之类的元素。