@@ -222,18 +222,165 @@ function loadAmplitude() {
222222 } )
223223 }
224224
225+ let previousPath = null
226+ let pageEnteredAt = Date . now ( )
227+ let scrollThresholdsFired = new Set ( )
228+ let dwellFiredForPath = null
229+ let scrollRafPending = false
230+ let contentEl = null
231+
232+ function elapsedSeconds ( ) {
233+ return Math . round ( ( Date . now ( ) - pageEnteredAt ) / 1000 )
234+ }
235+
236+ function maxScrollDepth ( ) {
237+ return scrollThresholdsFired . size ? Math . max ( ...scrollThresholdsFired ) : 0
238+ }
239+
225240 function trackPageView ( ) {
226- amplitude . track (
227- 'Page View: Docs New' ,
228- {
229- 'url' : window . location . href ,
230- 'title' : document . title ,
231- 'referrer' : document . referrer ,
232- 'path' : window . location . pathname ,
233- 'source' : 'docs' ,
234- 'source_detail' : '3.x'
241+ trackDwell ( )
242+
243+ const currentPath = window . location . pathname
244+ const props = {
245+ 'url' : window . location . href ,
246+ 'title' : document . title ,
247+ 'referrer' : document . referrer ,
248+ 'path' : currentPath ,
249+ 'source' : 'docs' ,
250+ 'source_detail' : '3.x'
251+ }
252+ if ( previousPath ) {
253+ props [ 'from_path' ] = previousPath
254+ }
255+ amplitude . track ( 'Page View: Docs New' , props )
256+ previousPath = currentPath
257+ pageEnteredAt = Date . now ( )
258+ scrollThresholdsFired = new Set ( )
259+ dwellFiredForPath = null
260+ contentEl = null
261+ window . removeEventListener ( 'scroll' , onScroll )
262+ window . addEventListener ( 'scroll' , onScroll , { passive : true } )
263+ }
264+
265+ function getScrollPercent ( ) {
266+ if ( ! contentEl ) contentEl = document . getElementById ( 'content-area' )
267+ if ( ! contentEl ) return 0
268+ const rect = contentEl . getBoundingClientRect ( )
269+ if ( rect . height <= 0 ) return 0
270+ // How much of the content bottom has been scrolled into the viewport
271+ const remaining = rect . bottom - window . innerHeight
272+ if ( remaining <= 0 ) return 100
273+ return Math . round ( ( ( rect . height - remaining ) / rect . height ) * 100 )
274+ }
275+
276+ function onScroll ( ) {
277+ if ( scrollRafPending || scrollThresholdsFired . size >= 4 ) return
278+ scrollRafPending = true
279+ requestAnimationFrame ( ( ) => {
280+ scrollRafPending = false
281+ const pct = getScrollPercent ( )
282+ for ( const t of [ 25 , 50 , 75 , 100 ] ) {
283+ if ( pct >= t && ! scrollThresholdsFired . has ( t ) ) {
284+ scrollThresholdsFired . add ( t )
285+ amplitude . track ( 'docs_scroll_depth' , {
286+ path : window . location . pathname ,
287+ scroll_depth : t ,
288+ time_on_page_s : elapsedSeconds ( )
289+ } )
290+ }
235291 }
236- )
292+ if ( scrollThresholdsFired . size >= 4 ) {
293+ window . removeEventListener ( 'scroll' , onScroll )
294+ }
295+ } )
296+ }
297+
298+ function trackDwell ( ) {
299+ const path = window . location . pathname
300+ if ( dwellFiredForPath === path ) return
301+ const seconds = elapsedSeconds ( )
302+ if ( seconds < 2 ) return
303+ dwellFiredForPath = path
304+ amplitude . track ( 'docs_dwell_time' , {
305+ path : path ,
306+ duration_s : seconds ,
307+ scroll_depth : maxScrollDepth ( )
308+ } )
309+ amplitude . flush ( )
310+ }
311+
312+ function initClickTracking ( ) {
313+ let searchDebounceTimer = null
314+
315+ document . addEventListener ( 'input' , ( e ) => {
316+ if ( ! e . target . matches ( '[cmdk-input]' ) ) return
317+ const path = window . location . pathname
318+ clearTimeout ( searchDebounceTimer )
319+ searchDebounceTimer = setTimeout ( ( ) => {
320+ const query = e . target . value . trim ( )
321+ if ( query . length >= 2 ) {
322+ amplitude . track ( 'docs_search' , {
323+ query : query . slice ( 0 , 200 ) ,
324+ path : path
325+ } )
326+ }
327+ } , 1000 )
328+ } )
329+
330+ document . addEventListener ( 'click' , ( e ) => {
331+ // Code copy button
332+ const copyBtn = e . target . closest ( '[data-testid="copy-code-button"]' )
333+ if ( copyBtn ) {
334+ const block = copyBtn . closest ( '[data-component-part="code-block-root"]' )
335+ const header = block ? block . querySelector ( '[data-component-part="code-block-header-filename"]' ) : null
336+ amplitude . track ( 'docs_code_copied' , {
337+ path : window . location . pathname ,
338+ code_title : header ? header . textContent . trim ( ) : null ,
339+ code_block_index : block
340+ ? Array . from ( document . querySelectorAll ( '[data-component-part="code-block-root"]' ) ) . indexOf ( block )
341+ : null
342+ } )
343+ return
344+ }
345+
346+ // Search result click (cmdk)
347+ const item = e . target . closest ( '[cmdk-item]' )
348+ if ( item ) {
349+ const input = document . querySelector ( '[cmdk-input]' )
350+ amplitude . track ( 'docs_search_result_clicked' , {
351+ query : input ? input . value . trim ( ) . slice ( 0 , 200 ) : null ,
352+ result_text : item . textContent . trim ( ) . slice ( 0 , 200 ) ,
353+ path : window . location . pathname
354+ } )
355+ return
356+ }
357+
358+ // AI assistant send button
359+ const sendBtn = e . target . closest ( '.chat-assistant-send-button' )
360+ if ( sendBtn ) {
361+ const textarea = document . getElementById ( 'chat-assistant-textarea' )
362+ if ( textarea && textarea . value . trim ( ) ) {
363+ amplitude . track ( 'docs_ai_search' , {
364+ query : textarea . value . trim ( ) . slice ( 0 , 200 ) ,
365+ path : window . location . pathname
366+ } )
367+ }
368+ return
369+ }
370+
371+ // Search bar open
372+ if ( e . target . closest ( '#search-bar-entry, #search-bar-entry-mobile' ) ) {
373+ amplitude . track ( 'docs_search_opened' , { path : window . location . pathname } )
374+ return
375+ }
376+
377+ // Feedback thumbs
378+ if ( e . target . closest ( '#feedback-thumbs-up' ) ) {
379+ amplitude . track ( 'docs_feedback' , { path : window . location . pathname , rating : 'positive' } )
380+ } else if ( e . target . closest ( '#feedback-thumbs-down' ) ) {
381+ amplitude . track ( 'docs_feedback' , { path : window . location . pathname , rating : 'negative' } )
382+ }
383+ } )
237384 }
238385
239386 const init = ( ) => {
@@ -256,6 +403,11 @@ function loadAmplitude() {
256403
257404 setTimeout ( addDeviceIdToAppLinks )
258405 observeRouteChanges ( trackPageView )
406+ document . addEventListener ( 'visibilitychange' , ( ) => {
407+ if ( document . visibilityState === 'hidden' ) trackDwell ( )
408+ } )
409+ window . addEventListener ( 'beforeunload' , trackDwell )
410+ initClickTracking ( )
259411 }
260412
261413 const url = 'https://cdn.amplitude.com/libs/analytics-browser-2.8.1-min.js.gz'
0 commit comments