- Bear Blog สร้าง ระบบวิเคราะห์ของตัวเอง ที่ทำงานได้โดยไม่ใช้ JavaScript ฝั่งไคลเอนต์ เนื่องจากข้อจำกัดด้านความเร็ว ประสิทธิภาพ และความเสถียร
- สคริปต์วิเคราะห์ทั่วไปอาจถูก ad blocker บล็อกได้ และถ้าใช้แค่ server logs ก็อาจมีทั้ง crawler, scraper และ parser ที่ใช้ GPT ปะปนเข้ามา ทำให้ สถิติผู้เข้าชมบิดเบือน
- เมื่อเกิด
body:hover ใน CSS ของแต่ละหน้า จะเรียก endpoint /hit/{{ post.id }}/ ผ่าน border-image และใช้ hover หรือการเลื่อนหน้าบนมือถือเป็น สัญญาณการอ่าน
- เซิร์ฟเวอร์ตรวจสอบจาก user-agent ว่าเป็นบ็อตหรือไม่ รวมถึงดูเบราว์เซอร์และแพลตฟอร์ม ส่วน IP address ใช้เฉพาะเพื่อระบุประเทศ จากนั้นลบการอ่านซ้ำด้วย แฮช IP+วันที่
- หากอ่านจากหลายอุปกรณ์ในวันเดียวกันภายใต้ IP เดียวกัน จะนับเป็นครั้งเดียว แต่ยังสามารถรวมยอด จำนวนการอ่านไม่ซ้ำ แยกตามหน้าได้อย่างเรียบง่ายโดยไม่เก็บข้อมูลระบุตัวตน
สร้างอีเวนต์การอ่านด้วย CSS hover
- ระบบวิเคราะห์ของ Bear Blog ทำตามข้อจำกัดที่ว่าไม่ใช้ JavaScript ฝั่งไคลเอนต์
- เครื่องมือวิเคราะห์ทั่วไปใช้ JavaScript ฝั่งไคลเอนต์เพื่อช่วยตัดสินบางส่วนว่าทราฟฟิกเป็นของจริงหรือเป็นบ็อต แต่ ad blocker จำนวนมากไม่ได้บล็อกแค่ Google Analytics เท่านั้น ยังบล็อกสคริปต์วิเคราะห์อย่าง Fathom และ Plausible ด้วย
- การ parse server logs เพียงอย่างเดียวอาจทำให้ crawler ของ search engine, scraper และ parser ที่ใช้ GPT ปะปนเหมือนเป็นทราฟฟิกปกติ จนเกิด มุมมองที่บิดเบือน
- Bear ใส่ CSS ต่อไปนี้ในแต่ละหน้า เพื่อให้เกิด
body:hover เมื่อผู้ใช้วางเคอร์เซอร์บนหน้า หรือเลื่อนหน้าบนมือถือ
body:hover {
border-image: url("/hit/{{ post.id }}/?ref={{ request.META.HTTP_REFERER }}");
}
- เมื่อ
body:hover ถูก trigger ก็จะมีการเรียก hit URL ของบทความนั้น
- ข้อมูลที่เพิ่มกลับเข้าไปอย่างชัดเจนใน request นี้คือ referrer และการสะกด
HTTP_REFERER เป็น รูปแบบที่สะกดผิดแต่ตกค้างมาเหมือนเป็นมาตรฐาน
- อาศัยข้อเท็จจริงที่ว่าบ็อตไม่ hover จึงใช้การเรียกแบบอิง
body:hover เป็นสัญญาณของ ผู้อ่านที่เป็นมนุษย์
- หลังจากนั้นเซิร์ฟเวอร์จะตรวจสอบว่า user-agent เป็นบ็อตหรือไม่ และดึงข้อมูลเบราว์เซอร์กับแพลตฟอร์มจากสตริง user-agent
ลบการอ่านซ้ำโดยไม่ใช้ข้อมูลระบุตัวตน
- ข้อจำกัดข้อที่สองคือไม่เก็บข้อมูลที่สามารถระบุตัวผู้อ่านได้ไว้ใน browser cookie หรือบนเซิร์ฟเวอร์
- IP address ใช้เฉพาะเพื่อระบุประเทศ และก่อนบันทึกจะ แฮช IP address ร่วมกับวันที่
- request ต่อ ๆ มาสำหรับหน้าเดียวกันจะถูกนำไปเทียบกับแฮช
IP address + วันที่ และทิ้งหากเป็นรายการซ้ำ
- ด้วยวิธีนี้ หนึ่ง IP address จะถูกนับเป็นหนึ่ง read ต่อหนึ่งหน้าในช่วงหนึ่งวัน
- จะไม่บันทึก IP address ต้นฉบับ และแฮชที่รวมวันที่ไว้จะมีผลเหมือนหมดอายุเป็นรายวัน
user_agent = httpagentparser.detect(self.request.META.get('HTTP_USER_AGENT', None))
if user_agent.get('bot', False):
print('Bot traffic')
return
ip_hash = hashlib.md5(f"{client_ip(self.request)}-{timezone.now().date()}".encode('utf-8')).hexdigest()
country = get_user_location(client_ip(self.request)).get('country_name', '')
device = user_agent.get('platform', {}).get('name', '')
browser = user_agent.get('browser', {}).get('name', '')
referrer = self.request.GET.get('ref', '')
if referrer:
referrer = urlparse(referrer)
referrer = '{uri.scheme}://{uri.netloc}/'.format(uri=referrer)
Hit.objects.get_or_create(
post_id=self.pk,
ip_address=ip_hash,
referrer=referrer,
country=country,
device=device,
browser=browser)
- IP hash ใช้เพียงเพื่อกัน hit ซ้ำภายในหนึ่งวัน และโดยพื้นฐานแล้วการดูหน้าทุกครั้งจะถูกจัดการแบบไม่ซ้ำกัน
- เมื่อสิ้นสุดแต่ละวัน งานเบื้องหลังจะลบแฮชออกจาก hit logs เพื่อไม่ให้ขัดกับการตีความ GDPR ที่เข้มงวดเกินไป
- ข้อเสียของวิธีนี้คือ หากอ่านจากหลายอุปกรณ์ในวันเดียวกันภายใต้ IP address เดียวกัน ก็จะถูกนับเป็น read เดียวเท่านั้น
- Bear มองว่ากรณีเช่นนี้เป็นเพียงส่วนน้อยของทราฟฟิก และเห็นว่าวิธีที่ใช้ CSS นี้ให้จำนวนการอ่านที่กระชับ เรียบง่าย และแม่นยำกว่าวิธีเก็บข้อมูลวิเคราะห์แบบอื่น ๆ หลายวิธี
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ผมเป็นผู้เขียนเอง แฮชของที่อยู่ IP ในบริบทนี้ใช้เพียงเพื่อป้องกันการนับการเข้าชมซ้ำภายในวันเดียวเท่านั้น
มีไว้เพื่อทำให้การเข้าชมหน้าแต่ละครั้งโดยพื้นฐานแล้วไม่ซ้ำกัน และเมื่อจบแต่ละวัน งาน worker จะล้างแฮชออกโดยยังคงข้อมูลการเข้าชมไว้ ผมได้เพิ่มการแก้ไขในบทความเพื่อชี้แจงเรื่องนี้แล้ว
ตอนเห็นไอเดียการใช้ request ที่เกิดจาก CSS มาทำ analytics ครั้งแรก รู้สึกว่ามันเจ๋งมาก
มีคนหนึ่งใน Twitter วางตารางสี่เหลี่ยมที่มองไม่เห็นไว้บนหน้า แล้วให้แต่ละช่องโหลดภาพพื้นหลังเฉพาะของตัวเองเมื่อ hover เพื่อใช้ทำ การติดตามเมาส์ ภาพพื้นหลังแต่ละภาพจะส่ง request เฉพาะไปยังเซิร์ฟเวอร์ แล้วเซิร์ฟเวอร์ตีความ request นั้น
ฤดูร้อนปีหนึ่ง ผมขยายไอเดียนี้เล่น ๆ จนทำ “เว็บแชตแบบอะซิงโครนัสที่ใช้เฉพาะ CSS” โดยไม่มี JavaScript: https://github.com/kkuchta/css-only-chat
การบอกว่าทำให้ที่อยู่ IP เป็นนิรนามด้วยการแฮชแค่วันที่กับ IP นั้นเป็นแค่ ละครความปลอดภัย
แฮชเชิงเข้ารหัสถูกออกแบบมาให้คำนวณได้เร็ว ใช้ hashcat บน MacBook M1 Pro คำนวณแฮช MD5 ได้ 6 พันล้านรายการต่อวินาที และที่อยู่ IPv4 มีแค่ 4 พันล้านรายการเท่านั้น สามารถ brute force ทั้งช่วงเพื่อหาที่อยู่ IP ได้ จึงแทบไม่ต่างจากการย้อนแฮช
ต่อให้ใช้แฮชที่ปลอดภัยอย่าง SHA-256 แทน MD5 ที่พังแล้ว ก็ยังเหมือนเดิม
ต่อให้แฮชชื่อนามสกุลของใครสักคน ภายหลังก็ยังตอบคำถามได้ว่า “แฮชนี้ตรงกับชื่อนามสกุลเฉพาะนี้หรือไม่” การตอบคำถามนี้ได้หมายความว่ากระบวนการทำให้เป็นนิรนามนั้นย้อนกลับได้
แฮชของที่อยู่ IP ในบริบทนี้ใช้เพียงเพื่อป้องกันการนับการเข้าชมซ้ำภายในวันเดียวเท่านั้น มีไว้เพื่อทำให้การเข้าชมหน้าแต่ละครั้งโดยพื้นฐานแล้วไม่ซ้ำกัน และเมื่อจบแต่ละวัน งาน worker จะลบแฮช IP ที่ไม่จำเป็นอีกต่อไปออก
[0] https://news.ycombinator.com/item?id=37596757
จะเก็บ salt ถาวรหรือหมุนเปลี่ยนเป็นระยะ ๆ นั้นเป็นเพียงรายละเอียดการ implement และประเด็นสำคัญเมื่อใส่ salt ให้แฮชเพื่อ analytics คือ salt ต้องไม่ออกจากฝั่งไคลเอนต์เด็ดขาด
จากคำอธิบายในบทความดูเหมือนจะไม่มี salt หรืออาจใช้วันที่ปัจจุบันเป็นเหมือน salt ซึ่งไม่ใช่ salt แบบสุ่ม ดังนั้นใครก็ตามที่อยากรู้ว่า “IP x.y.z.w เข้ามาในวันที่ yy-mm-dd หรือไม่?” ก็เดาได้ง่าย
ถ้ามองจากมุมของผู้โจมตี จะตัดสินปัญหาแบบนี้ได้ง่าย ถ้ามีข้อมูลชุดนี้แล้วจะหาข้อมูลอะไรเกี่ยวกับบุคคลหนึ่งได้อย่างไร? ถ้าทำไม่ได้ ข้อมูลนั้นโดยทั่วไปก็มีแนวโน้มจะเก็บได้อย่างปลอดภัย
อย่างไรก็ตาม ผมกังวลว่าแค่ ละครความปลอดภัย แบบนี้อาจเพียงพอให้ผ่านกฎหมายและข้อบังคับด้านความเป็นส่วนตัวหลายฉบับได้
ดูฉลาดดี แต่
body:hoverมีแนวโน้มจะพลาด ผู้ใช้ที่ใช้แต่คีย์บอร์ด และ user agent ที่ไม่ใช้อุปกรณ์ชี้ตำแหน่ง กล่าวคือผู้ใช้เทคโนโลยีช่วยเหลือ แทบจะทั้งหมดแม้กลุ่มเหล่านี้อาจเป็นกลุ่มชายขอบ แต่การเห็นว่าพวกเขาถูกกีดกันไม่ทางใดก็ทางหนึ่งเป็นสัญญาณที่แย่มากเสมอ
ผมไม่แน่ใจ และค่อนข้างสงสัยด้วยว่า CSS พื้นฐานล้วน ๆ จะมีวิธีตรวจจับได้อย่างเชื่อถือได้ 100% ในทุก user agent ว่า “ผู้ใช้จริงกำลังอ่านบทความนี้อยู่” แล้วส่ง HTTP request หรือไม่ บางตัวอาจไม่รองรับ CSS เลย หรืออาจปิดการโหลดภาพตกแต่งของ CSS ไว้
selector สมัยใหม่ที่อาจช่วยได้คือ
:root:focus-withinแต่ผู้ใช้ต้องโฟกัสไปที่องค์ประกอบที่โต้ตอบได้จริง และ user agent ทั้งหมดก็ไม่ได้รับประกันเรื่องนี้ แม้แต่@scroll-timelineซึ่งเป็นแอนิเมชันผูกกับการเลื่อนหน้าจอระดับล้ำสมัย ก็ยังมีแนวโน้มที่จะพลาดผู้อ่านอักษรเบรลล์อยู่ดี:hoverซึ่งก็คือ มากกว่า 50% ของ user agent หรือ? แน่นอนว่าในกรณีที่ไม่ได้เสียบเมาส์พื้นที่นี้คือความกว้างที่รวมคอลัมน์เนื้อหากับ padding ข้างละ 20px ดังนั้นผู้ใช้คีย์บอร์ดบางส่วนจะถูกจับได้ ส่วนผู้ใช้เมาส์บางส่วน โดยเฉพาะผู้ใช้ viewport ขนาดใหญ่ จะไม่ถูกจับได้
ที่บอกว่า “ไม่ใช่แค่สิ่งแย่ ๆ อย่าง Google Analytics แต่ Fathom กับ Plausible ก็มีปัญหาในการบันทึกกิจกรรมบนเบราว์เซอร์ที่บล็อกโฆษณา” ผมมองว่าเป็นเพราะพวกเขาพยายามเอาตัวรอดอยู่ใน ดินแดนรกร้างอันเป็นพิษ โดยแท้
ผู้ใช้อย่างเราเบื่อแนวคิดทั้งหมดนี้แล้ว และถ้า analytics แบบ CSS ได้รับความนิยม ผมคิดว่าก็จะมีความพยายามหาทางเลี่ยงมันเช่นกัน
ผมปลดบล็อก Piwik/Matomo, Plausible, Fathom ใน uBlock ด้วยตัวเอง ผมไม่เห็นว่าสิ่งที่พวกเขาติดตามและวิธีติดตามนั้นเป็นอันตราย และมันให้ข้อมูลที่เป็นประโยชน์แก่ผู้ดูแลไซต์เพื่อ “ปรับปรุงบริการ”
ตัวอย่างเช่น Plausible เก็บข้อมูลเกี่ยวกับผมน้อยกว่าล็อก nginx หรือ Apache ทั่วไปเสียอีก ในมุมของบล็อกเกอร์ การเห็นว่าบทความถูกโพสต์ขึ้น HN หรือไม่ ถูกลิงก์จากที่ไหนสักแห่งหรือไม่ คอนเทนต์แบบไหนถูกมองว่ามีคุณค่าและแบบไหนถูกมองข้าม เป็นเรื่องสำคัญ เพราะจะได้เขียนสิ่งที่คนอยากอ่านจริง ๆ และกระจายผ่านช่องทางที่รับรู้ได้จริง
access.logของเว็บเซิร์ฟเวอร์ไปใส่ในบริการวิเคราะห์ไม่ได้ป้องกันอะไรเลยกลับกัน ตัวเลขอาจพองขึ้นได้ เพราะแทบเป็นไปไม่ได้เลยที่จะกรอง ทราฟฟิกบอท ทั้งหมดโดยดูแค่ user agent
ผมไม่รู้ว่าจริง ๆ แล้วมีคนกลัวเรื่องนี้มากแค่ไหน ปฏิกิริยาน่าจะแตกต่างกันตั้งแต่ “ใช่ น่าขนลุก” ไปจนถึง “ไร้สาระ นั่นมันแค่ไซไฟ”
เรื่องนี้เป็นที่รู้จักกันมาหลายสิบปีแล้วในชื่อ พิกเซลติดตาม
:hoverจะช่วยกรองบอทที่ไม่ได้ใช้ WebDriver แบบเต็มรูปแบบออกไปได้ ซึ่งก็คือบอทส่วนใหญ่ในบางแง่ วิธีโหลดไฟล์
.cssที่แทบจะว่างเปล่าผ่าน@importพร้อมกับsupportsอาจดีกว่าด้วยซ้ำ ad blocker จับพิกเซลติดตามโปร่งใส 1px ได้เก่งมาก แต่มีแนวโน้มจะบล็อกไฟล์.cssน้อยกว่าเพื่อไม่ให้เลย์เอาต์พัง อย่างไรก็ตาม กรณีนี้ก็จะเสียข้อดีอันชาญฉลาดของ:hoverไปเป็นคำถามที่สงสัยจริง ๆ แต่กลัวว่าจะอ่านแล้วเหมือนความเห็นที่ไม่ให้ค่า เป้าหมายของการเก็บข้อมูลวิเคราะห์บน บล็อกส่วนตัวที่ไม่ใช่เชิงพาณิชย์ แบบที่ดูเหมือน Bearblog คืออะไร?
เหตุผลหลักที่สนใจ analytics คือเพื่อดูว่าบทความถูกอ่านหรือไม่ ภายนอก และในบางแง่ก็เพราะความหลงตัวเอง แต่จริง ๆ แล้วมันเกี่ยวกับ ความเชื่อมโยงระหว่างผู้เขียนกับผู้อ่าน ผมอยากรู้จริง ๆ ว่าผู้อ่านตอบสนองต่ออะไร และอยากมอบสิ่งนั้นให้มากขึ้น “สิ่งนั้น” อาจเป็นหัวข้อ น้ำเสียง หรือความยาวก็ได้ มันช่วยปรับแต่งเนื้อหาให้เหมาะกับผู้อ่านของผม ท้ายที่สุดแล้ว ผมเขียนได้สิบสองหัวข้อในยี่สิบสี่วิธี แน่นอนว่าผมเขียนสิ่งที่ชอบ แต่ก็ปรับให้สะท้อนกับผู้อ่านได้ดีขึ้น
ในความหมายนี้ analytics ก็เป็นวิธีทำความรู้จักผู้อ่านด้วย สำหรับบล็อกที่มี engagement สูง analytics ให้ภาพร่างบุคลิกที่เลือนรางของผู้อ่าน ไม่ใช่แค่เห็นว่าพวกเขาชอบอะไร แต่ยังเห็นว่าชอบเมื่อไรด้วย เห็นได้ว่าอ่านเป็นอย่างแรกในตอนเช้า อ่านตอนพักเที่ยง หรืออ่านดึก ๆ ซึ่งช่วยในการตัดสินใจว่าจะโพสต์ในเวลาหนึ่ง ๆ หรือช่วยยืนยันการตัดสินใจนั้น แน่นอนว่าทั้งหมดเป็นข้อมูลที่เลือนราง แต่ช่วยให้เชื่อมโยงกับผู้อ่านได้อย่างกระตือรือร้นขึ้นจริง ๆ
แน่นอนว่ามันใช้กับโฆษณาได้และถูกใช้ในทางที่ผิดได้ แต่ถ้าผมต้องการฟีดแบ็กต่อสิ่งที่ทำอยู่ มันก็จำเป็น
ไม่ว่าไซต์จะมีคนอ่าน 12 คนหรือ 12,000 คน มันอาจไม่มีมูลค่าทางการเงินก็ได้ แต่ในมุมส่วนตัว การรู้ว่าผู้คนอยากอ่านอะไรจากผมเป็นเรื่องดี ทำให้รู้สึกว่าเวลาที่ใช้เขียนถูกใช้ไปอย่างคุ้มค่า และถ้าต้องการก็ปรับไปในทางที่ได้รับความนิยมมากขึ้นได้
แม้จะเป็นบล็อกเกอร์ส่วนตัว ก็อาจอยากปรับคอนเทนต์ให้เข้ากับผู้อ่าน การรู้ว่าบทความหัวข้อหนึ่งมีคนอ่าน 500 คน แต่อีกหัวข้อมีแค่ 3 คนอ่าน เป็นเรื่องดี
ต้นปีนี้ผมเคยลองทำอะไรแบบนี้ แต่หมดแรงจูงใจระหว่างทำเว็บ UI วิธีของผมไม่ใช่ CSS แต่เป็นการโหลด รูปภาพปลอม ผ่านแท็กแบบง่าย ๆ
https://github.com/nolytics
ทำไมไม่ดึงข้อมูลนี้จาก HTTP server ไปเลย?
“ตัวเลือกในการ parse server log มีอยู่เสมอ และพอจะทำให้เห็นคร่าว ๆ ว่าทราฟฟิกแบบไหนเข้าถึงเซิร์ฟเวอร์ แต่โดยทั่วไปทราฟฟิกทั้งหมดของเซิร์ฟเวอร์จะดูเหมือนกันหมด ในทางเทคนิค บอทควรมี user agent ที่ระบุว่าตัวเองเป็นบอท แต่เพราะพวกมันพยายามกวาดข้อมูลให้เหมือน ‘คน’ ที่ใช้เบราว์เซอร์ จึงแทบไม่ระบุตัวเองแบบนั้น โดยพื้นฐานแล้ว ถ้าใช้แค่ server log ในการวิเคราะห์ คุณจะเห็นทราฟฟิกบิดเบี้ยว เพราะมีทั้ง crawler ของเสิร์ชเอนจิน, scraper และตอนนี้ยังมี parser ที่ใช้ GPT อีกมากมาย”
ข้อมูลวิเคราะห์เก็บไว้อย่างไร?
สมมติว่ามีไซต์อีคอมเมิร์ซและมีสินค้าที่ต้องการขาย นอกจาก analytics แล้ว ก็ตัดสินใจบันทึกพฤติกรรมบางอย่างเอง เช่น การเข้าชมหน้ารายละเอียดสินค้าในสถานะที่ล็อกอินอยู่ ดังนั้นจึงต้องการเก็บสิ่งต่าง ๆ เช่น ID ผู้ใช้, ID สินค้า, timestamp
แล้วในทางปฏิบัติควรเก็บอย่างไร? ตอนแรกคิดแบบง่าย ๆ ว่าใส่ลงตารางก็พอ DBA ถามว่าต้องใช้ข้อมูลนานแค่ไหน และตอบไปว่าอย่างน้อยหนึ่งเดือน จากนั้นเขาก็บอกว่าโอเค และดูเหมือนว่าอาจตั้งงานให้ย้ายข้อมูลที่เก่ากว่านั้นไปยังตารางอื่นไว้แล้ว
ในทางปฏิบัติแล้ว log แบบนี้เก็บกันอย่างไร และเก็บไว้นานแค่ไหน?
เคยทำแบบนี้มาก่อน และจนกว่าจะถึงประมาณ 1 พันล้านแถวก็ยังไม่จำเป็นต้องคิดเรื่อง partitioning ด้วยซ้ำ ถึงอย่างนั้นก็ควรทำ partitioning ให้เร็วกว่านั้น ประสบการณ์ครั้งนั้นไม่สนุกเลย
ทำ aggregation ได้เร็วกว่ามาก และจัดการกับคอลัมน์จำนวนมากที่มีข้อมูลกระจัดกระจายได้ดี เช่น event
paidมีแอตทริบิวต์amountส่วน eventpage_viewมีแอตทริบิวต์urlเป็นต้นข้อดีของ TimescaleDB คือมันจัดการสร้าง materialized view สำหรับ aggregation ที่สนใจ เช่น จำนวนการดูสินค้าต่อชั่วโมง ให้เอง หากมี event จำนวนมากและไม่อยากให้ฐานข้อมูลใหญ่เกินไป ก็เลือก “ทิ้ง” ตัว event เอง แล้วเหลือไว้เฉพาะ aggregation ได้