Repository: PostHog/posthog
Pull request #30652: fix: static-in-app heatmap wasn't setting wildcard type on URL search
Author: login: pauldambra
Created: 2025-04-01T18:27:31Z | Merged: 2025-04-02T11:17:00Z

in the toolbar we detect whether to regex query the URL by comparing to the current page url

in the static replay heatmap we'll need to probe for regex directly

![2025-04-01 23 09 11](https://github.com/user-attachments/assets/5aafade5-304d-4271-ace2-9cb85cd3d5e9)



Diff:
diff --git a/frontend/__snapshots__/scenes-app-insights-user-paths--user-paths-edit--dark.png b/frontend/__snapshots__/scenes-app-insights-user-paths--user-paths-edit--dark.png
index 8e104a1e9057..a724a09438b5 100644
Binary files a/frontend/__snapshots__/scenes-app-insights-user-paths--user-paths-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights-user-paths--user-paths-edit--dark.png differ
diff --git a/frontend/src/lib/components/heatmaps/heatmapDataLogic.ts b/frontend/src/lib/components/heatmaps/heatmapDataLogic.ts
index 416ee11bf1ce..f02d9b7dc851 100644
--- a/frontend/src/lib/components/heatmaps/heatmapDataLogic.ts
+++ b/frontend/src/lib/components/heatmaps/heatmapDataLogic.ts
@@ -35,10 +35,7 @@ export const heatmapDataLogic = kea<heatmapDataLogicType>([
         patchHeatmapFilters: (filters: Partial<HeatmapFilters>) => ({ filters }),
         setHeatmapFixedPositionMode: (mode: HeatmapFixedPositionMode) => ({ mode }),
         setHeatmapColorPalette: (Palette: string | null) => ({ Palette }),
-        setHref: (href: string) => ({ href }),
-        setUrlMatch: (match: 'exact' | 'regex') => ({
-            match,
-        }),
+        setHref: (href: string, match: 'exact' | 'regex') => ({ href, match }),
         setFetchFn: (fetchFn: 'native' | 'toolbar') => ({ fetchFn }),
         setHeatmapScrollY: (scrollY: number) => ({ scrollY }),
         setWindowWidthOverride: (widthOverride: number | null) => ({ widthOverride }),
@@ -84,15 +81,9 @@ export const heatmapDataLogic = kea<heatmapDataLogicType>([
             },
         ],
         href: [
-            null as string | null,
+            null as { href: string | null; match: 'exact' | 'regex' } | null,
             {
-                setHref: (_, { href }) => href,
-            },
-        ],
-        urlMatch: [
-            'exact' as 'exact' | 'regex',
-            {
-                setUrlMatch: (_, { match }) => match,
+                setHref: (_, { href, match }) => ({ href, match }),
             },
         ],
         heatmapScrollY: [
@@ -114,12 +105,14 @@ export const heatmapDataLogic = kea<heatmapDataLogicType>([
             {
                 resetHeatmapData: () => ({ results: [] }),
                 loadHeatmap: async (_, breakpoint) => {
-                    const href = values.href
-                    const matchType = values.urlMatch
+                    if (!values.href) {
+                        return null
+                    }
+                    await breakpoint(150)
+
+                    const { href, match } = values.href
                     const { date_from, date_to, filter_test_accounts } = values.commonFilters
                     const { type, aggregation } = values.heatmapFilters
-                    const urlExact = matchType === 'exact' ? href : undefined
-                    const urlRegex = matchType === 'regex' ? href : undefined
 
                     // toolbar fetch collapses queryparams but this URL has multiple with the same name
                     const apiURL = `/api/heatmap/${encodeParams(
@@ -127,8 +120,8 @@ export const heatmapDataLogic = kea<heatmapDataLogicType>([
                             type,
                             date_from,
                             date_to,
-                            url_exact: urlExact,
-                            url_pattern: urlRegex,
+                            url_exact: match === 'exact' ? href : undefined,
+                            url_pattern: match === 'regex' ? href : undefined,
                             viewport_width_min: values.viewportRange.min,
                             viewport_width_max: values.viewportRange.max,
                             aggregation,
@@ -137,8 +130,8 @@ export const heatmapDataLogic = kea<heatmapDataLogicType>([
                         '?'
                     )}`
 
-                    breakpoint()
                     const response = await (values.fetchFn === 'toolbar' ? toolbarFetch(apiURL, 'GET') : fetch(apiURL))
+                    breakpoint()
 
                     if (response.status === 403) {
                         toolbarConfigLogic.actions.authenticate()
diff --git a/frontend/src/lib/utils/regexp.ts b/frontend/src/lib/utils/regexp.ts
index c3bc19cb9252..8990a7630035 100644
--- a/frontend/src/lib/utils/regexp.ts
+++ b/frontend/src/lib/utils/regexp.ts
@@ -6,3 +6,21 @@ export const isValidRegexp = (regex: string): boolean => {
         return false
     }
 }
+
+function isLikelyRegex(url: string): boolean {
+    // Common regex special characters that indicate regex pattern
+    const regexSpecialChars = /[.*+?^${}()|[\]\\]/
+
+    // Check for common regex anchors
+    const hasAnchors = url.startsWith('^') || url.endsWith('$')
+
+    // Check for regex special characters
+    const hasSpecialChars = regexSpecialChars.test(url)
+
+    return hasAnchors || hasSpecialChars
+}
+
+// Combine both checks if you want to be extra sure
+export function isValidRegexPattern(s: string): boolean {
+    return isLikelyRegex(s) && isValidRegexp(s)
+}
diff --git a/frontend/src/scenes/heatmaps/heatmapsBrowserLogic.ts b/frontend/src/scenes/heatmaps/heatmapsBrowserLogic.ts
index 070f55b6d370..11853290501c 100644
--- a/frontend/src/scenes/heatmaps/heatmapsBrowserLogic.ts
+++ b/frontend/src/scenes/heatmaps/heatmapsBrowserLogic.ts
@@ -16,6 +16,7 @@ import {
 } from 'lib/components/IframedToolbarBrowser/utils'
 import { LemonBannerProps } from 'lib/lemon-ui/LemonBanner'
 import { objectsEqual } from 'lib/utils'
+import { isValidRegexPattern } from 'lib/utils/regexp'
 import posthog from 'posthog-js'
 import { RefObject } from 'react'
 import { removeReplayIframeDataFromLocalStorage } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic'
@@ -59,7 +60,7 @@ export const heatmapsBrowserLogic = kea<heatmapsBrowserLogicType>([
             heatmapDataLogic,
             ['heatmapEmpty'],
         ],
-        actions: [heatmapDataLogic, ['loadHeatmap', 'setFetchFn', 'setHref', 'setUrlMatch']],
+        actions: [heatmapDataLogic, ['loadHeatmap', 'setFetchFn', 'setHref']],
     }),
 
     actions({
@@ -289,7 +290,7 @@ export const heatmapsBrowserLogic = kea<heatmapsBrowserLogicType>([
             if (replayIframeData && replayIframeData.url) {
                 // we don't want to use the toolbar fetch or the iframe message approach
                 actions.setFetchFn('native')
-                actions.setHref(replayIframeData.url)
+                actions.setHref(replayIframeData.url, isValidRegexPattern(replayIframeData.url) ? 'regex' : 'exact')
             } else {
                 removeReplayIframeDataFromLocalStorage()
             }
@@ -416,6 +417,15 @@ export const heatmapsBrowserLogic = kea<heatmapsBrowserLogicType>([
             }
         },
 
+        setReplayIframeDataURL: async ({ url }, breakpoint) => {
+            await breakpoint(150)
+            if (url?.trim().length) {
+                // we don't want to use the toolbar fetch or the iframe message approach
+                actions.setFetchFn('native')
+                actions.setHref(url, isValidRegexPattern(url) ? 'regex' : 'exact')
+            }
+        },
+
         setBrowserUrl: ({ url }) => {
             actions.maybeLoadTopUrls()
             if (url?.trim().length) {
diff --git a/frontend/src/toolbar/elements/heatmapToolbarMenuLogic.ts b/frontend/src/toolbar/elements/heatmapToolbarMenuLogic.ts
index 89fa90799659..3218e339d67b 100644
--- a/frontend/src/toolbar/elements/heatmapToolbarMenuLogic.ts
+++ b/frontend/src/toolbar/elements/heatmapToolbarMenuLogic.ts
@@ -58,7 +58,6 @@ export const heatmapToolbarMenuLogic = kea<heatmapToolbarMenuLogicType>([
                 'setCommonFilters',
                 'setHeatmapFixedPositionMode',
                 'setHref as setDataHref',
-                'setUrlMatch',
                 'resetHeatmapData',
                 'patchHeatmapFilters',
                 'loadHeatmap',
@@ -78,9 +77,9 @@ export const heatmapToolbarMenuLogic = kea<heatmapToolbarMenuLogicType>([
 
         loadMoreElementStats: true,
         setMatchLinksByHref: (matchLinksByHref: boolean) => ({ matchLinksByHref }),
-        loadAllEnabled: (delayMs: number = 0) => ({ delayMs }),
-        maybeLoadClickmap: (delayMs: number = 0) => ({ delayMs }),
-        maybeLoadHeatmap: (delayMs: number = 0) => ({ delayMs }),
+        loadAllEnabled: true,
+        maybeLoadClickmap: true,
+        maybeLoadHeatmap: true,
     }),
     windowValues(() => ({
         windowWidth: (window: Window) => window.innerWidth,
@@ -117,6 +116,8 @@ export const heatmapToolbarMenuLogic = kea<heatmapToolbarMenuLogicType>([
             {
                 resetElementStats: () => emptyElementsStatsPages,
                 getElementStats: async ({ url }, breakpoint) => {
+                    await breakpoint(150)
+
                     const { href, wildcardHref } = values
                     let defaultUrl: string = ''
                     if (!url) {
@@ -150,6 +151,7 @@ export const heatmapToolbarMenuLogic = kea<heatmapToolbarMenuLogicType>([
                         undefined,
                         url ? 'use-as-provided' : 'full'
                     )
+                    breakpoint()
 
                     if (response.status === 403) {
                         toolbarConfigLogic.actions.authenticate()
@@ -157,7 +159,6 @@ export const heatmapToolbarMenuLogic = kea<heatmapToolbarMenuLogicType>([
                     }
 
                     const paginatedResults = await response.json()
-                    breakpoint()
 
                     if (!Array.isArray(paginatedResults.results)) {
                         throw new Error('Error loading HeatMap data!')
@@ -331,7 +332,7 @@ export const heatmapToolbarMenuLogic = kea<heatmapToolbarMenuLogicType>([
     })),
     subscriptions(({ actions }) => ({
         viewportRange: () => {
-            actions.maybeLoadHeatmap(500)
+            actions.maybeLoadHeatmap()
         },
     })),
     listeners(({ actions, values }) => ({
@@ -345,22 +346,18 @@ export const heatmapToolbarMenuLogic = kea<heatmapToolbarMenuLogicType>([
             toolbarPosthogJS.capture('toolbar mode triggered', { mode: 'heatmap', enabled: false })
         },
 
-        loadAllEnabled: async ({ delayMs }, breakpoint) => {
-            await breakpoint(delayMs)
-
+        loadAllEnabled: async () => {
             actions.maybeLoadHeatmap()
             actions.maybeLoadClickmap()
         },
 
-        maybeLoadClickmap: async ({ delayMs }, breakpoint) => {
-            await breakpoint(delayMs)
-            if (values.heatmapEnabled && values.clickmapsEnabled) {
+        maybeLoadClickmap: async () => {
+            if (values.clickmapsEnabled) {
                 actions.getElementStats()
             }
         },
 
-        maybeLoadHeatmap: async ({ delayMs }, breakpoint) => {
-            await breakpoint(delayMs)
+        maybeLoadHeatmap: async () => {
             if (values.heatmapEnabled) {
                 if (values.heatmapFilters.enabled && values.heatmapFilters.type) {
                     actions.loadHeatmap()
@@ -369,17 +366,15 @@ export const heatmapToolbarMenuLogic = kea<heatmapToolbarMenuLogicType>([
         },
 
         setHref: ({ href }) => {
-            actions.setDataHref(href)
-            actions.setUrlMatch('exact')
-            actions.loadAllEnabled()
+            actions.setDataHref(href, 'exact')
+            actions.maybeLoadClickmap()
         },
         setWildcardHref: ({ href }) => {
-            actions.setDataHref(href)
-            actions.setUrlMatch('regex')
-            actions.loadAllEnabled(1000)
+            actions.setDataHref(href, 'regex')
+            actions.maybeLoadClickmap()
         },
         setCommonFilters: () => {
-            actions.loadAllEnabled(200)
+            actions.loadAllEnabled()
         },
 
         // Only trigger element stats loading if clickmaps are enabled
@@ -400,12 +395,10 @@ export const heatmapToolbarMenuLogic = kea<heatmapToolbarMenuLogicType>([
                 // Clear the heatmap if the type changes
                 actions.resetHeatmapData()
             }
-            actions.maybeLoadHeatmap(200)
+            actions.maybeLoadHeatmap()
         },
     })),
     afterMount(({ actions, values, cache }) => {
-        actions.loadAllEnabled()
-
         cache.scrollCheckTimer = setInterval(() => {
             const scrollY = values.posthog?.scrollManager?.scrollY() ?? 0
             if (values.heatmapScrollY !== scrollY) {


Reviews:
user: login: pauldambra
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T22:50:26Z
user: login: pauldambra
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T22:50:44Z
user: login: pauldambra
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T22:54:04Z
user: login: greptile-apps[bot]
body: 
## PR Summary

This PR refines URL handling for heatmaps by consolidating href actions and improving regex validation.

- Consolidated URL handling in frontend/src/lib/components/heatmaps/heatmapDataLogic.ts by unifying href with its match type.
- Updated frontend/src/scenes/heatmaps/heatmapsBrowserLogic.ts to use isValidRegexPattern for proper regex/static matching.
- Enhanced regex helper functions in frontend/src/lib/utils/regexp.ts to better detect intended regex queries.
- Refactored URL matching in frontend/src/toolbar/elements/heatmapToolbarMenuLogic.ts to remove redundant actions.

<sub>4 file(s) reviewed, no comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=posthog_30652)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T22:55:17Z
user: login: pauldambra
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T23:26:22Z
user: login: pauldambra
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T23:32:52Z
user: login: veryayskiy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T10:46:36Z
user: login: veryayskiy
body: see the comments
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T10:53:00Z

Review comments:
diff_hunk: @@ -35,10 +35,7 @@ export const heatmapDataLogic = kea<heatmapDataLogicType>([
         patchHeatmapFilters: (filters: Partial<HeatmapFilters>) => ({ filters }),
         setHeatmapFixedPositionMode: (mode: HeatmapFixedPositionMode) => ({ mode }),
         setHeatmapColorPalette: (Palette: string | null) => ({ Palette }),
-        setHref: (href: string) => ({ href }),
-        setUrlMatch: (match: 'exact' | 'regex') => ({
-            match,
path: frontend/src/lib/components/heatmaps/heatmapDataLogic.ts
user: login: pauldambra
body: these always get called together and were causing duplicate loads
created_at: 2025-04-01T22:50:26Z
author_association: MEMBER
diff_hunk: @@ -137,8 +129,8 @@ export const heatmapDataLogic = kea<heatmapDataLogicType>([
                         '?'
                     )}`
 
-                    breakpoint()
                     const response = await (values.fetchFn === 'toolbar' ? toolbarFetch(apiURL, 'GET') : fetch(apiURL))
+                    breakpoint()
path: frontend/src/lib/components/heatmaps/heatmapDataLogic.ts
user: login: pauldambra
body: breakpoint should be after the network call
created_at: 2025-04-01T22:50:44Z
author_association: MEMBER
diff_hunk: @@ -416,6 +417,15 @@ export const heatmapsBrowserLogic = kea<heatmapsBrowserLogicType>([
             }
         },
 
+        setReplayIframeDataURL: async ({ url }, breakpoint) => {
+            await breakpoint(150)
path: frontend/src/scenes/heatmaps/heatmapsBrowserLogic.ts
user: login: pauldambra
body: don't trigger on every character typed
created_at: 2025-04-01T22:54:04Z
author_association: MEMBER
diff_hunk: @@ -114,21 +105,23 @@ export const heatmapDataLogic = kea<heatmapDataLogicType>([
             {
                 resetHeatmapData: () => ({ results: [] }),
                 loadHeatmap: async (_, breakpoint) => {
-                    const href = values.href
-                    const matchType = values.urlMatch
+                    if (!values.href) {
+                        return null
+                    }
+                    await breakpoint(150)
path: frontend/src/lib/components/heatmaps/heatmapDataLogic.ts
user: login: pauldambra
body: we were doing lots of breakpoint awaiting above this... just do it in one place
created_at: 2025-04-01T23:26:22Z
author_association: MEMBER
path: frontend/src/products.tsx
user: login: pauldambra
body: this file is autogenerated... so I guess we can ignore it 🤷 
it updated after i merged from master 
created_at: 2025-04-01T23:32:52Z
author_association: MEMBER
diff_hunk: @@ -6,3 +6,21 @@ export const isValidRegexp = (regex: string): boolean => {
         return false
     }
 }
+
+function isLikelyRegex(url: string): boolean {
+    // Common regex special characters that indicate regex pattern
+    const regexSpecialChars = /[.*+?^${}()|[\]\\]/
+
+    // Check for common regex anchors
+    const hasAnchors = url.startsWith('^') || url.endsWith('$')
+
+    // Check for regex special characters
+    const hasSpecialChars = regexSpecialChars.test(url)
+
+    return hasAnchors || hasSpecialChars
+}
+
+// Combine both checks if you want to be extra sure
+export function isValidRegexPattern(s: string): boolean {
path: frontend/src/lib/utils/regexp.ts
user: login: veryayskiy
body: We can use regex ai here. Not a blocker.
created_at: 2025-04-02T10:46:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-01T18:39:36Z
body: **Size Change:** -262 B (0%) 

**Total Size:** 9.96 MB



<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size | Change |
| :--- | :---: | :---: |
| `frontend/dist/toolbar.js` | 9.96 MB | -262 B (0%) |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-04-01T18:56:53Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 7](https://github.com/PostHog/posthog/pull/30652/commits/b48fab50188bfe409b7acce11ce678f181e67e80))
- `webkit`: **0** added, **0** modified, **0** deleted

Triggered by [this commit](https://github.com/PostHog/posthog/pull/30652/commits/4ca1e358088c5a957267de0585be7b04d8584315).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/30652/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-04-01T23:04:31Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 1](https://github.com/PostHog/posthog/pull/30652/commits/69c19c77f3bbec8c08071ee07fe33940abafcb90))
- `webkit`: **0** added, **0** modified, **0** deleted

Triggered by [this commit](https://github.com/PostHog/posthog/pull/30652/commits/ab236f3849e2dce21a2a827a878360c0e68ed5b5).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/30652/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-04-01T23:14:50Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 1](https://github.com/PostHog/posthog/pull/30652/commits/79562b05e40f5839def0139c3a63f80c1428487e))
- `webkit`: **0** added, **0** modified, **0** deleted

Triggered by [this commit](https://github.com/PostHog/posthog/pull/30652/commits/19a7401baea106a81d4416f2b68980c0ecf4e92b).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/30652/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-04-02T00:12:12Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 1](https://github.com/PostHog/posthog/pull/30652/commits/aaac95b274a60a3f5904da443dbc68dd3239c314))
- `webkit`: **0** added, **0** modified, **0** deleted

Triggered by [this commit](https://github.com/PostHog/posthog/pull/30652/commits/e3b31abb04aaac7474342772bce86b94431ae16f).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/30652/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-04-02T00:23:12Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 1](https://github.com/PostHog/posthog/pull/30652/commits/2b5a8588aeb61a01499ebcb1b3aa25552bd7129a))
- `webkit`: **0** added, **0** modified, **0** deleted

Triggered by [this commit](https://github.com/PostHog/posthog/pull/30652/commits/2edeef3708da33e82e6ff6b310145826084f8996).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/30652/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: veryayskiy
created_at: 2025-04-02T10:50:06Z
body: Another real UI issue is:
<img width="1485" alt="Screenshot 2025-04-02 at 12 48 54" src="https://github.com/user-attachments/assets/8bbbae53-7e4d-4824-a110-50c374f2d473" />

I picked a URL, then I see a cursor there, but whatever i type - nothing happens. 

It's not clear at all that I need to click on "Edit" icon on the right side to be able to edit.


I think it's OK to ship, but that UX can be improved for sure :)

For example,

You can add a dropdown on the left here that gives the list, but whatever is paste in the URL is editable straight away:

<img width="406" alt="Screenshot 2025-04-02 at 12 51 20" src="https://github.com/user-attachments/assets/23184cb9-d695-4f06-8ee2-a5939668c635" />

author_association: CONTRIBUTOR
user: login: pauldambra
created_at: 2025-04-02T11:16:56Z
body: Ah, yeah... that's "just" the standard lemon input select but for sure the UI isn't finished 🙈 
author_association: MEMBER