ข้ามไปยังเนื้อหา

RUM และ Real User Context

RUM จับสิ่งที่ visitor จริงทำในเบราว์เซอร์หรือแอปของตัวเองเป็น structured event และตอนมัน propagate trace context เข้าไปใน backend call ที่ action ของ visitor นั้นสั่ง มันขยาย correlation ออกไปอีกหนึ่ง hop เลยจาก backend trace ไปถึงตัวคนที่เป็นต้นเหตุ

Real User Monitoring (RUM) รันเป็น SDK ข้างในเบราว์เซอร์หรือ mobile app เอง ไม่ใช่บน server ของเรา ตอน visitor จริงโต้ตอบกับหน้าเว็บหรือแอป มันปล่อย structured event type เล็ก ๆ กลุ่มหนึ่ง:

  • session — หนึ่งการเข้าชม ตั้งแต่เริ่มจนจบ
  • view — หน้า (หรือ screen บน mobile) ที่ user ไปถึง
  • action — คลิก, แตะ, submit form
  • error — JS exception หรือ resource load ที่ fail จับฝั่ง client

นี่คือมุมมองที่ต่างจาก synthetic monitoring (เบราว์เซอร์ที่ script ไว้แกล้งเป็น user ตามตารางเวลา ครอบไว้ที่อื่นในคอร์สนี้) RUM คือเบราว์เซอร์จริงของ visitor จริง แบบ real time เลยสะท้อนสภาพ network จริง อุปกรณ์จริง และเส้นทางจริงที่ user เดินผ่านผลิตภัณฑ์ — ไม่ใช่เส้นทางในอุดมคติของ script เดียว

src/rum-init.js
import { datadogRum } from '@datadog/browser-rum';
datadogRum.init({
applicationId: '<RUM_APP_ID>',
clientToken: '<RUM_CLIENT_TOKEN>',
site: '<DATADOG_SITE>',
service: 'checkout-web',
sessionSampleRate: 100,
sessionReplaySampleRate: 0
});

ติดตั้ง @datadog/[email protected] ตัวจริงปัจจุบัน (npm view @datadog/browser-rum version) แล้วเช็ค shape นี้กับ .d.ts ที่ shipped มาเองของมัน (RumInitConfiguration ใน configuration.d.ts) — ทุก key ตรงนี้มีจริง interface เดียวกันนั้นมี JSDoc ของตัวเองที่แสดงสองวิธีที่ SDK ตัวนี้โหลดจริง: npm import แล้วเรียก datadogRum.init({...}) หรือ CDN <script> tag ที่ expose global window.DD_RUM แล้วเรียก DD_RUM.init({...}) ด้วย option object แบบเดียวกันเป๊ะ SDK ตัวเดียวกัน init shape เดียวกัน แค่สองวิธีเอา script เข้าหน้า — ขั้นตอน build ของ npm bundle หรือ <script> tag ที่ไม่ต้อง build เลย

ตัวมันเองแค่ RUM action หรือ view เป็นแค่ frontend — บอกแค่ว่า user คลิก “Pay now” ไม่บอกว่าเกิดอะไรขึ้นที่ server หลังจากนั้น payoff ของ correlation คือตอนที่ RUM SDK ถูก config ให้ trace backend URL ชุดหนึ่ง มันแปะ trace context header แบบเดียวกับที่ backend tracer มองหาเข้าไปใน XHR/fetch call ขาออก ทำให้ span ของ backend service ที่สร้างขึ้นมา ลิงก์กลับไปที่ RUM view/action ตัวนั้นเป๊ะ

src/rum-init-traced.js
import { datadogRum } from '@datadog/browser-rum';
datadogRum.init({
applicationId: '<RUM_APP_ID>',
clientToken: '<RUM_CLIENT_TOKEN>',
site: '<DATADOG_SITE>',
service: 'checkout-web',
allowedTracingUrls: [
(url) => url.startsWith('https://api.example.com')
]
});

Session Replay สร้างภาพจำลองระดับ pixel ว่า user เห็นและทำอะไรจริง ๆ จาก RUM session เดียวกัน — ไม่ได้ถูกรวมมาอัตโนมัติแค่เพราะ RUM ถูก init เช็ค default จริงจาก .d.ts แทนที่จะสมมติเอาเอง:

sessionSampleRate @defaultValue 100 -- RUM เอง: เปิดโดย default
sessionReplaySampleRate @defaultValue 0 -- Session Replay: ปิดโดย default
defaultPrivacyLevel @defaultValue mask-user-input

sessionReplaySampleRate default เป็น 0 — Session Replay เป็น opt-in ไม่ใช่อะไรที่เปิดเงียบ ๆ ทันทีที่ RUM เปิด defaultPrivacyLevel default เป็น mask-user-input (ค่าจริงจาก source ที่ shipped เดียวกัน: allow, mask, mask-user-input, mask-unless-allowlisted) — หมายความว่า ตั้งแต่แกะกล่อง Session Replay mask ข้อความที่ user พิมพ์เข้าโดย default แทนที่จะบันทึกแบบเปิดเผย เป็น default ที่คำนึงถึง privacy ไม่ใช่แบบ opt-out เปิดการบันทึก replay เป็นอีกหนึ่ง init key:

src/rum-init-replay.js
import { datadogRum } from '@datadog/browser-rum';
datadogRum.init({
applicationId: '<RUM_APP_ID>',
clientToken: '<RUM_CLIENT_TOKEN>',
site: '<DATADOG_SITE>',
service: 'checkout-web',
sessionSampleRate: 100,
sessionReplaySampleRate: 20,
defaultPrivacyLevel: 'mask'
});

docs.datadoghq.com/real_user_monitoring/session_replay/browser/privacy_options คือแหล่งอ้างอิงหลักว่าแต่ละ privacy level mask อะไรจริง ๆ (เฉพาะ form input, หรือ text content ทั้งหมด, หรือ explicit allowlist) — ควรอ่านก่อนเปิด replay สำหรับหน้าไหนก็ตามที่จัดการข้อมูล user จริง เพราะแม้แต่ default ที่เข้มที่สุด (mask-user-input) ก็ยังบันทึกโครงสร้าง DOM กับ text ที่ไม่ใช่ input รอบ ๆ อยู่ดี

sessionSampleRate คุมว่า session สัดส่วนไหนที่ RUM track เลย (@defaultValue 100 — ทุก session โดย default) sessionReplaySampleRate คุมว่าใน session ที่ ถูก sample ไปแล้ว นั้น สัดส่วนไหนที่ได้ replay recording เพิ่มด้วย (@defaultValue 0) มันรวมกันแบบทบ: sessionSampleRate: 50, sessionReplaySampleRate: 20 หมายความว่าประมาณ 50% ของ session ถูก track โดย RUM เลย และใน 50% นั้น ประมาณ 20% ได้ replay เพิ่ม — ไม่ใช่ 20% ของทุก session

src/rum-set-user.js
import { datadogRum } from '@datadog/browser-rum';
datadogRum.setUser({
id: 'user-48213',
name: 'A. Vetavos',
});

.d.ts จริง (rumPublicApi.d.ts) ประกาศ overload สองแบบ: setUser(newUser: User & { id: string }) คือ shape ปัจจุบันที่แนะนำ — id จำเป็น — และ overload ที่สอง @deprecated รับ User ที่ไม่มี id เลย เก็บไว้แค่เพื่อ backward compatibility RUM event ทุกตัวที่ปล่อยหลังเรียกอันนี้จะพก user context ไว้ใต้ attribute @usr ซึ่งเป็นสิ่งที่ทำให้ filter RUM (และผ่าน correlation แบบเดียวกับที่บทนี้ครอบ ไปถึง backend trace ที่ session พวกนั้นสั่ง) ลงไปที่ “ทุกอย่างที่ user คนนี้คนเดียวเจอ” ได้ — มีประโยชน์เวลา reproduce บั๊กที่ลูกค้าคนหนึ่งรายงานมา โดยไม่ต้องให้เขาอธิบายว่าคลิกอะไรบ้างเป๊ะ ๆ

backend trace อย่างเดียวตอบแค่ “span ไหนบ้าง ข้าม service ไหนบ้าง ที่จัดการ request นี้” — นั่นคือเรื่องทั้งหมดที่ module APM เล่าไป RUM ขยายไอเดีย trace_id เดียวกันออกไปอีกหนึ่ง hop เลยขอบ infrastructure ของเรา ทำให้ identifier ตัวเดียวกันที่ผูก span เข้าด้วยกันข้าม service ก็ผูกกลับไปที่ RUM view/session ด้วย — ตอบว่า “user จริงคนไหน หน้าไหน คลิกอะไร ที่ทำให้ trace นี้เกิดขึ้นมาตั้งแต่แรก” บทนี้ตั้งใจให้เบา ๆ — RUM เป็นหัวข้อใหญ่พอจะเป็นคอร์สของตัวเองได้เลย — แต่ไอเดีย correlation เป็นไอเดียเดียวกับที่ module นี้สร้างมาตลอด: shared identifier แทนการเดาเอง

flowchart LR
  U[Real user in browser] --> Act["RUM action: click 'Pay now'"]
  Act --> View[RUM view/session]
  Act --> Req["Traced fetch/XHR call\n(trace context injected)"]
  Req --> Span[Backend service span]
  Span --> Trace[APM distributed trace]
  View -.linked to.-> Trace
จากการคลิกหนึ่งครั้งไปยัง distributed trace

🐛 หา bug ในโค้ดนี้

// AI comment: "Wire up RUM so we can see what users are doing on checkout"
import { datadogRum } from '@datadog/browser-rum';
datadogRum.init({
applicationId: '<RUM_APP_ID>',
clientToken: '<RUM_CLIENT_TOKEN>',
site: '<DATADOG_SITE>',
sessionSampleRate: 100,
});
datadogRum.setUser({ name: 'checkout-user' });
เฉลย

โค้ดนี้ compile โหลด และเริ่มบันทึก session/view/action จริงได้ทันที — ไม่มีอะไรดูพังเลย มีสองช่องว่างจริงที่พิสูจน์แล้วข้างบนเทียบกับ .d.ts และเอกสารจริง อย่างแรก: ไม่มี service และไม่มี allowedTracingUrls — doc comment บน service บอกว่า allowedTracingUrls ต้องการมันชัดเจน และไม่มี key ไหนมี default ที่จะเปิด RUM-to-APM linking ได้เอง ทุก backend trace ที่ checkout flow นี้สั่งจึงหลุดจาก RUM session ที่เป็นต้นเหตุโดยสิ้นเชิง — เงียบ ๆ โดยที่ RUM เองทำงานสมบูรณ์ตลอดเวลา อย่างที่สอง: setUser({ name: 'checkout-user' }) ใช้ overload ที่ @deprecated (ไม่มี id) — มันยังรันได้ แต่ @usr context ของ RUM event ทุกตัวขาด field เดียว (id) ที่ทำให้ pivot ไปที่ “ทุกอย่างที่ user คนนี้ทำ” ได้อย่างน่าเชื่อถือ เพราะชื่อชนกันได้ แต่ ID ไม่ การแก้: เพิ่ม service: 'checkout-web' กับ list allowedTracingUrls ที่ตรงกับ origin จริงของ backend และส่ง id จริงเข้า setUser

มองว่า RUM เป็นกล้องตัวที่สองที่ส่องเหตุการณ์เดียวกันจากมุมที่ต่างไปจาก backend tracing โดยสิ้นเชิง กล้องของ backend tracing อยู่ข้างใน infrastructure ของเรา คอยดู span ส่งต่อกัน กล้องของ RUM อยู่ข้างในเบราว์เซอร์ของ user คอยดูคลิกกับ page load ไม่มีกล้องตัวไหนตอบคำถามทั้งหมดได้คนเดียว allowedTracingUrls คือสิ่งเดียวที่ต่อฟีดกล้องสองตัวเข้าเป็นช็อตเดียวต่อเนื่อง — ถ้าไม่มีมัน เราจะมีการบันทึกที่ดีสมบูรณ์แยกกันสองชุดของ incident เดียวกัน ที่ไม่เคยถูกตัดต่อเข้าด้วยกันเลย และไม่มีฝั่งไหนบ่นเลยว่าฟุตเทจของอีกกล้องหลุดออกไป

  • sessionSampleRate: 100 เทียบกับค่าที่ต่ำกว่า — sample ทุก session ให้ coverage ของ RUM ครบสมบูรณ์ แต่ต้นทุนเพิ่มตามสัดส่วน (RUM คิดราคาต่อ tracked session); rate ที่ต่ำกว่าประหยัดต้นทุน แลกกับการเห็นแค่ statistical slice ของพฤติกรรม user จริง ซึ่งสำคัญกว่าสำหรับหน้าที่ traffic น้อยเทียบกับหน้าที่ traffic เยอะ
  • sessionReplaySampleRate เปิดเทียบกับปิด (default 0) — Session Replay คือเครื่องมือ debug ที่มีประโยชน์ทาง visual มากที่สุดที่ RUM มีให้ (เห็นสิ่งที่ user เห็นเป๊ะ) แต่ถูกคิดราคาและ sample แยกจาก RUM เองด้วยเหตุผล: การบันทึก เก็บ และ render วิดีโอ session เต็มรูปแบบแพงกว่า structured event เปล่า ๆ มากต่อ session ซึ่งเป็นเหตุผลที่ SDK ตั้ง default เป็นปิดแทนที่จะเลียนแบบท่าที default-เปิดของ sessionSampleRate
  • defaultPrivacyLevel: 'mask-user-input' (default) เทียบกับ 'mask' หรือ 'allow' — default ที่เข้มกว่าปกป้อง form input ที่ sensitive (password, เลขบัตร) อัตโนมัติ แลกกับความแม่นยำของ replay บางส่วน; ผ่อนเป็น allow บันทึกทุกอย่างตามจริง แต่ย้ายความรับผิดชอบทั้งหมดในการไม่จับข้อมูล sensitive ไปให้คนที่ config หน้านั้น
  • “init RUM แล้ว Session Replay จะบันทึกอัตโนมัติ” ไม่จริง — พิสูจน์แล้วข้างบนจาก .d.ts จริง: sessionReplaySampleRate default เป็น 0 structured event ของ RUM (session/view/action/error) กับการบันทึกแบบวิดีโอของ Session Replay ถูก sample และคิดราคาแยกจากกัน
  • “allowedTracingUrls อย่างเดียวพอที่จะเชื่อม RUM กับ APM” ไม่จริง — พิสูจน์แล้วข้างบน: doc comment ของ init option service เองระบุว่า allowedTracingUrls ยังต้องการ service ที่ config ชัดเจนอยู่ดี ต้องมีทั้งคู่ ขาดอันใดอันหนึ่งไม่มี error เลย มีแค่ telemetry สองชุดที่หลุดจากกัน
  • “window.DD_RUM กับ export datadogRum ของ npm เป็น SDK คนละตัวที่ความสามารถต่างกัน” ไม่จริง — พิสูจน์แล้วข้างบนจาก JSDoc ของ .d.ts ที่ shipped มาเอง: เป็น SDK ตัวเดียวกัน init() shape เดียวกัน โหลดสองวิธี (CDN <script> tag เทียบกับ npm import)
  • “setUser({ name: ... }) ที่ไม่มี id ดีพอ ๆ กับการส่ง id เข้าไป” ไม่จริง — พิสูจน์แล้วข้างบน: overload นั้นถูก mark @deprecated ชัดเจนใน .d.ts จริง เพราะชื่อไม่ใช่ identifier ที่เชื่อถือได้ id จริงต่างหากที่ทำให้ view แบบ “ทุกอย่างที่ user คนนี้ทำ” ที่ filter ได้ทำงานจริง
ความต่างหลักระหว่าง RUM กับ synthetic monitoring คืออะไร
ข้อไหน "ไม่ใช่" event type แบบ structured ที่ RUM จับ
ทีมหนึ่ง config allowedTracingUrls ถูกต้อง แต่ไม่ได้ตั้ง service ชัดเจนใน RUM init call .d.ts จริงของ @datadog/browser-rum 7.15.0 บอกว่าเกิดอะไรขึ้น
sessionReplaySampleRate default เป็นเท่าไหร่ พิสูจน์แล้วข้างบนจาก .d.ts จริงที่ shipped มา และมันหมายความว่าอะไรในทางปฏิบัติ
setUser({ name: 'checkout-user' }) (ไม่มี field id) ถูกเรียกแทนที่จะเป็น setUser({ id: 'user-48213', name: '...' }) .d.ts จริงบอกว่าอะไร
RUM ขยาย correlation story ที่ backend trace อย่างเดียวสร้างไว้ยังไง