From 2a567e6d9a38ce87279754c847a38bfecfdbe03c Mon Sep 17 00:00:00 2001 From: Jeremy Zilar Date: Thu, 30 Apr 2026 10:24:24 -0400 Subject: [PATCH 1/2] Attach well_id to PostHog pageviews on well detail routes. Parse Ocotillo and AMP well show URLs so $pageview includes well_id, page_template, and well_detail_area for breakdowns. Extend feature_used on Ocotillo WellShow with well_detail_area. --- src/analytics/posthog.ts | 38 +++++++++++++++++++- src/components/analytics/PostHogPageview.tsx | 9 +++-- src/pages/ocotillo/thing/well-show.tsx | 6 +++- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/analytics/posthog.ts b/src/analytics/posthog.ts index 6f5d8d9d..fb44fe03 100644 --- a/src/analytics/posthog.ts +++ b/src/analytics/posthog.ts @@ -28,12 +28,48 @@ export const initPostHog = () => { initialized = true } -export const capturePostHogPageview = (path: string) => { +/** + * Optional properties for well detail pages so `well_id` is on `$pageview` + * (and shows up in PostHog when breaking down or filtering). + */ +export const wellDetailPageviewProps = ( + pathname: string +): + | { + well_id: string + page_template: 'well_detail' + well_detail_area: 'ocotillo' | 'amp' + } + | undefined => { + const ocotillo = pathname.match(/^\/ocotillo\/well\/show\/([^/]+)\/?$/) + if (ocotillo) { + return { + well_id: ocotillo[1], + page_template: 'well_detail', + well_detail_area: 'ocotillo', + } + } + const amp = pathname.match(/^\/amp\/wells\/show\/([^/]+)\/?$/) + if (amp) { + return { + well_id: amp[1], + page_template: 'well_detail', + well_detail_area: 'amp', + } + } + return undefined +} + +export const capturePostHogPageview = ( + path: string, + extras?: Record +) => { if (!isEnabled || !initialized) return posthog.capture('$pageview', { $current_url: window.location.href, path, + ...(extras ?? {}), }) } diff --git a/src/components/analytics/PostHogPageview.tsx b/src/components/analytics/PostHogPageview.tsx index 93e7cf73..4a123c5c 100644 --- a/src/components/analytics/PostHogPageview.tsx +++ b/src/components/analytics/PostHogPageview.tsx @@ -1,6 +1,10 @@ import { useEffect, useRef } from 'react' import { useLocation } from 'react-router' -import { capturePostHogPageview, initPostHog } from '@/analytics/posthog' +import { + capturePostHogPageview, + initPostHog, + wellDetailPageviewProps, +} from '@/analytics/posthog' export const PostHogPageview = () => { const location = useLocation() @@ -15,7 +19,8 @@ export const PostHogPageview = () => { if (lastPathRef.current === path) return - capturePostHogPageview(path) + const wellDetail = wellDetailPageviewProps(location.pathname) + capturePostHogPageview(path, wellDetail) lastPathRef.current = path }, [location.hash, location.pathname, location.search]) diff --git a/src/pages/ocotillo/thing/well-show.tsx b/src/pages/ocotillo/thing/well-show.tsx index a9fcc791..2889e0d7 100644 --- a/src/pages/ocotillo/thing/well-show.tsx +++ b/src/pages/ocotillo/thing/well-show.tsx @@ -65,7 +65,11 @@ export const WellShow = () => { useEffect(() => { if (id) - captureEvent('feature_used', { feature: 'well_detail', well_id: id }) + captureEvent('feature_used', { + feature: 'well_detail', + well_id: id, + well_detail_area: 'ocotillo', + }) }, [id]) const detailsQuery = useQuery({ From 4c0cd52328aa3747423663fb04499d7adb7d322a Mon Sep 17 00:00:00 2001 From: Jeremy Zilar Date: Thu, 30 Apr 2026 10:36:13 -0400 Subject: [PATCH 2/2] Add PostHog global_search events and unmask command palette in session replay. Emit global_search after API and docs searches with query, result_count, has_results, and had_error. Mask other inputs in replay but show the palette field via data-posthog-unmask-search and maskInputFn. --- src/analytics/posthog.ts | 15 ++++++++ src/components/SearchModal.tsx | 5 ++- src/hooks/useSearchModalState.ts | 63 ++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/analytics/posthog.ts b/src/analytics/posthog.ts index fb44fe03..4dd2c96a 100644 --- a/src/analytics/posthog.ts +++ b/src/analytics/posthog.ts @@ -19,6 +19,21 @@ export const initPostHog = () => { capture_pageview: false, capture_pageleave: true, capture_exceptions: true, + session_recording: { + maskInputFn: (text, element) => { + const el = element as HTMLInputElement | undefined + if (el?.type === 'password') { + return '*'.repeat(text.length) + } + if ( + el?.hasAttribute?.('data-posthog-unmask-search') || + el?.closest?.('[data-posthog-unmask-search]') + ) { + return text + } + return '*'.repeat(text.length) + }, + }, }) // Tag every event with the environment so staging visits are diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 7b605045..52e01b21 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -84,7 +84,10 @@ export const SearchModal = ({ open, onClose }: SearchModalProps) => { placeholder="Search" fullWidth sx={{ fontSize: 15 }} - inputProps={{ 'aria-label': 'Search' }} + inputProps={{ + 'aria-label': 'Search', + 'data-posthog-unmask-search': true, + }} endAdornment={ state.query ? ( diff --git a/src/hooks/useSearchModalState.ts b/src/hooks/useSearchModalState.ts index 1686d777..07552ab4 100644 --- a/src/hooks/useSearchModalState.ts +++ b/src/hooks/useSearchModalState.ts @@ -1,5 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useGo } from '@refinedev/core' +import { captureEvent } from '@/analytics/posthog' import { GroupType } from '@/constants' import { useAbortableList } from './useAbortableList' import { useDebounce } from './useDebounce' @@ -109,6 +110,68 @@ export const useSearchModalState = ({ return searchDocs(parsed.term) }, [parsed]) + /** Avoid duplicate PostHog emissions when the debounced query or outcome repeats. */ + const defaultSearchEmittedKey = useRef(null) + useEffect(() => { + defaultSearchEmittedKey.current = null + }, [debounced]) + + /** + * PostHog: command palette API search (wells, contacts, assets). + * One event per completed search for a given debounced query string. + */ + useEffect(() => { + if (!open || parsed.mode !== 'default') return + const q = debounced.trim() + if (!q) return + if (searchQuery.isFetching) return + + const key = `${q}|${searchQuery.isError ? 1 : 0}|${results.length}` + if (defaultSearchEmittedKey.current === key) return + defaultSearchEmittedKey.current = key + + captureEvent('global_search', { + search_mode: 'default', + query: q, + result_count: results.length, + has_results: results.length > 0, + had_error: searchQuery.isError, + }) + }, [ + debounced, + open, + parsed.mode, + results.length, + searchQuery.isError, + searchQuery.isFetching, + ]) + + const docsSearchEmittedKey = useRef(null) + useEffect(() => { + docsSearchEmittedKey.current = null + }, [parsed.term]) + + /** + * PostHog: local docs search (!docs …). + */ + useEffect(() => { + if (!open || parsed.mode !== 'docs') return + const term = parsed.term.trim() + if (!term) return + + const key = `${term}|${docsResults.length}` + if (docsSearchEmittedKey.current === key) return + docsSearchEmittedKey.current = key + + captureEvent('global_search', { + search_mode: 'docs', + query: term, + result_count: docsResults.length, + has_results: docsResults.length > 0, + had_error: false, + }) + }, [docsResults.length, open, parsed.mode, parsed.term]) + const navigateToResult = (option: SearchResult) => { switch (option.group) { case GroupType.Wells: