Dashboard filtering, automatic tracking, chart rebuild & settings modal #40

Merged
uz1mani merged 28 commits from staging into main 2026-03-07 00:21:04 +00:00
uz1mani commented 2026-03-07 00:17:42 +00:00 (Migrated from github.com)

Summary

  • Add dashboard filtering with click-to-filter, hover percentages, and a compact filter dropdown with real data values
  • Add automatic tracking for AI traffic sources, outbound links, file downloads, 404 errors, and scroll depth
  • Rebuild the chart component from scratch with cleaner stat cards, proper Y-axis, and streamlined toolbar
  • Replace the settings page with a modal-based layout and redesign the Campaigns panel

Changes

Dashboard Filtering

  • Dimension filtering by browser, country, page, device, OS, referrer, and UTM parameters
  • Click any item in any panel to instantly filter the dashboard
  • Hover percentage indicators on Pages, Locations, Technology, and Referrers
  • Compact "Filter" button with dropdown showing real values and visitor counts (up to 100)
  • Solid brand-colored filter pills with duplicate prevention
  • Custom event properties with per-event property breakdown

Automatic Tracking (Script Features)

  • AI traffic source identification (ChatGPT, Perplexity, Claude, Gemini, Copilot, DeepSeek, Grok, Meta AI, You.com, Phind)
  • Automatic outbound link and file download tracking
  • Automatic 404 detection and scroll depth tracking (25/50/75/100%)

Chart Rebuild

  • Rebuilt Chart.tsx from scratch — removed sparklines, wider Y-axis with integer-only ticks, lighter grid, streamlined toolbar with icon-only export
  • Removed avg reference line and badge
  • Fixed PNG export (CSS var resolution via getComputedStyle)
  • Fixed tabular-nums causing font fallback on KPI numbers

Dashboard UI

  • Campaigns panel: row-based layout in half-width grid, always-visible UTM medium/campaign
  • Underline tab switchers on Pages, Locations, Technology
  • "View all" moved to bottom of list
  • Renamed "Content" → "Pages", "Top Referrers" → "Referrers"

Settings Modal

  • Replaced full settings page with SettingsModal
  • Shared settings components, borderless profile section
  • Renamed section to "Account", moved Danger Zone to own sidebar item

Other

  • Bumped @ciphera-net/ui to ^0.0.80
  • Consolidated changelog (merged 0.13.1-alpha into 0.13.0-alpha, removed developer-facing entries)

Test Plan

  • Open dashboard — verify chart renders with proper Y-axis, no sparklines, no avg line
  • Click any item in Pages/Referrers/Locations/Technology — verify filter is applied
  • Hover items — verify percentage appears with smooth slide-in
  • Click "Filter" button — verify dropdown shows dimensions with real values
  • Add multiple filters — verify no duplicates, pills are removable
  • Verify Campaigns panel shows UTM medium/campaign with em-dashes when empty
  • Export chart as PNG — verify correct background in light and dark mode
  • Check KPI numbers use Plus Jakarta Sans (no font fallback)
  • Open settings modal — verify Account section, Danger Zone in sidebar
  • Verify AI sources (e.g. ChatGPT referrer) show proper icon and name in Referrers
## Summary - Add dashboard filtering with click-to-filter, hover percentages, and a compact filter dropdown with real data values - Add automatic tracking for AI traffic sources, outbound links, file downloads, 404 errors, and scroll depth - Rebuild the chart component from scratch with cleaner stat cards, proper Y-axis, and streamlined toolbar - Replace the settings page with a modal-based layout and redesign the Campaigns panel ## Changes **Dashboard Filtering** - Dimension filtering by browser, country, page, device, OS, referrer, and UTM parameters - Click any item in any panel to instantly filter the dashboard - Hover percentage indicators on Pages, Locations, Technology, and Referrers - Compact "Filter" button with dropdown showing real values and visitor counts (up to 100) - Solid brand-colored filter pills with duplicate prevention - Custom event properties with per-event property breakdown **Automatic Tracking (Script Features)** - AI traffic source identification (ChatGPT, Perplexity, Claude, Gemini, Copilot, DeepSeek, Grok, Meta AI, You.com, Phind) - Automatic outbound link and file download tracking - Automatic 404 detection and scroll depth tracking (25/50/75/100%) **Chart Rebuild** - Rebuilt `Chart.tsx` from scratch — removed sparklines, wider Y-axis with integer-only ticks, lighter grid, streamlined toolbar with icon-only export - Removed avg reference line and badge - Fixed PNG export (CSS var resolution via `getComputedStyle`) - Fixed `tabular-nums` causing font fallback on KPI numbers **Dashboard UI** - Campaigns panel: row-based layout in half-width grid, always-visible UTM medium/campaign - Underline tab switchers on Pages, Locations, Technology - "View all" moved to bottom of list - Renamed "Content" → "Pages", "Top Referrers" → "Referrers" **Settings Modal** - Replaced full settings page with `SettingsModal` - Shared settings components, borderless profile section - Renamed section to "Account", moved Danger Zone to own sidebar item **Other** - Bumped `@ciphera-net/ui` to ^0.0.80 - Consolidated changelog (merged 0.13.1-alpha into 0.13.0-alpha, removed developer-facing entries) ## Test Plan - [x] Open dashboard — verify chart renders with proper Y-axis, no sparklines, no avg line - [x] Click any item in Pages/Referrers/Locations/Technology — verify filter is applied - [x] Hover items — verify percentage appears with smooth slide-in - [x] Click "Filter" button — verify dropdown shows dimensions with real values - [x] Add multiple filters — verify no duplicates, pills are removable - [x] Verify Campaigns panel shows UTM medium/campaign with em-dashes when empty - [ ] Export chart as PNG — verify correct background in light and dark mode - [x] Check KPI numbers use Plus Jakarta Sans (no font fallback) - [x] Open settings modal — verify Account section, Danger Zone in sidebar - [x] Verify AI sources (e.g. ChatGPT referrer) show proper icon and name in Referrers
greptile-apps[bot] commented 2026-03-07 00:21:12 +00:00 (Migrated from github.com)

Greptile Summary

This is a large, well-structured PR that adds dimension filtering, automatic event tracking (404, scroll depth, outbound links, file downloads), a rebuilt chart, and a settings modal. The SWR layer and API functions are consistently updated to carry filters through every data-fetching path, and the new AddFilterDropdown and FilterBar components handle deduplication and URL state cleanly.

Key issues to address before merging:

  • Filter serialization delimiter collision (lib/filters.ts)| and , are used as raw separators without encoding, so any filter value containing those characters (referrer URLs, page paths with query strings, city names like "Portland, OR") will be silently truncated or split incorrectly during parseFiltersFromURL. This affects a core user-facing feature.
  • Previous-period comparison ignores active filters (app/sites/[id]/page.tsx line 232)prevStats is fetched without filtersParam, meaning the KPI percentage deltas compare filtered current data against unfiltered historical data, producing misleading numbers whenever a filter is active.
  • EventProperties values fetch has no error handling or loading state — if the API call for property values fails, stale values from the previously selected key remain visible without any indication of failure.
  • Outbound link and file download events omit the destination URL — the new props parameter could trivially carry the destination url, making these events actionable instead of just counts.
  • '404' event name is unconventional — the validation regex allows it, but 'page_not_found' would be more consistent with the documented naming convention and render more cleanly in the dashboard UI.

Confidence Score: 3/5

  • Safe to merge after fixing the filter serialization delimiter bug and the unfiltered previous-period stats comparison, as both would produce incorrect data for users.
  • The PR is well-engineered overall — SWR cache keys, API signatures, and component contracts are all consistently updated. The score is docked primarily because the filter serialization format has a structural bug that will silently corrupt filter state for real-world values (URLs with |, city names with ,), and the previous-period KPI comparison will show misleading deltas whenever a filter is active. Both issues are in the critical filtering feature that is the centerpiece of this PR.
  • lib/filters.ts (delimiter encoding) and app/sites/[id]/page.tsx line 232 (prevStats missing filters) need attention before this is safe to ship.

Important Files Changed

Filename Overview
lib/filters.ts New filter serialization utility — delimiter collision bug: `
app/sites/[id]/page.tsx Main dashboard page extended with filters, event properties, and scroll depth panels; prevStats fetched without active filters causes misleading KPI period-over-period comparisons when filters are applied.
public/script.js Added 404 detection, scroll depth, outbound link, file download, and custom event props tracking — well-guarded with opt-out attributes; outbound/download events omit destination URL props, and '404' event name is unconventional.
components/dashboard/EventProperties.tsx New component for per-event property breakdown; property-values fetch lacks a loading indicator and error handler, which can leave stale data visible on key switch failure.
components/dashboard/AddFilterDropdown.tsx New two-step filter dropdown with async suggestion fetching, cancellation via cleanup flag, keyboard and outside-click handling — well-structured with no issues found.
lib/swr/dashboard.ts All SWR hooks correctly updated to accept and thread filters into both the cache key and fetcher — cache invalidation on filter change is handled properly.
lib/api/stats.ts Uniform filters parameter added to all private dashboard API functions; new event-property endpoints are well-typed and follow existing patterns.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    User([User]) -->|clicks item / uses Filter dropdown| AddFilterDropdown

    AddFilterDropdown -->|onFetchSuggestions| API[(API\ngetTopPages / getCountries / etc.)]
    API -->|FilterSuggestion list| AddFilterDropdown
    AddFilterDropdown -->|DimensionFilter| handleAddFilter

    handleAddFilter -->|dedup check| filters[(filters state)]
    filters -->|serializeFilters| filtersParam[filtersParam\nstring]

    filtersParam -->|URL sync\nreplaceState| URL[/URL ?filters=.../]
    filtersParam -->|SWR cache key| SWRHooks

    SWRHooks -->|filters query param| BackendAPI[(Backend API)]
    BackendAPI -->|filtered data| SWRHooks

    SWRHooks -->|data| DashComponents[Dashboard Panels\nPages · Referrers · Locations\nTech · Goals · Chart]

    DashComponents -->|onFilter| handleAddFilter

    FilterBar -->|onRemove / onClear| filters
    filters --> FilterBar

    URL -->|parseFiltersFromURL on init| filters

Last reviewed commit: 8ebd8ba

<h3>Greptile Summary</h3> This is a large, well-structured PR that adds dimension filtering, automatic event tracking (404, scroll depth, outbound links, file downloads), a rebuilt chart, and a settings modal. The SWR layer and API functions are consistently updated to carry `filters` through every data-fetching path, and the new `AddFilterDropdown` and `FilterBar` components handle deduplication and URL state cleanly. Key issues to address before merging: - **Filter serialization delimiter collision (`lib/filters.ts`)** — `|` and `,` are used as raw separators without encoding, so any filter value containing those characters (referrer URLs, page paths with query strings, city names like "Portland, OR") will be silently truncated or split incorrectly during `parseFiltersFromURL`. This affects a core user-facing feature. - **Previous-period comparison ignores active filters (`app/sites/[id]/page.tsx` line 232)** — `prevStats` is fetched without `filtersParam`, meaning the KPI percentage deltas compare filtered current data against unfiltered historical data, producing misleading numbers whenever a filter is active. - **`EventProperties` values fetch has no error handling or loading state** — if the API call for property values fails, stale values from the previously selected key remain visible without any indication of failure. - **Outbound link and file download events omit the destination URL** — the new `props` parameter could trivially carry the destination `url`, making these events actionable instead of just counts. - **`'404'` event name is unconventional** — the validation regex allows it, but `'page_not_found'` would be more consistent with the documented naming convention and render more cleanly in the dashboard UI. <h3>Confidence Score: 3/5</h3> - Safe to merge after fixing the filter serialization delimiter bug and the unfiltered previous-period stats comparison, as both would produce incorrect data for users. - The PR is well-engineered overall — SWR cache keys, API signatures, and component contracts are all consistently updated. The score is docked primarily because the filter serialization format has a structural bug that will silently corrupt filter state for real-world values (URLs with `|`, city names with `,`), and the previous-period KPI comparison will show misleading deltas whenever a filter is active. Both issues are in the critical filtering feature that is the centerpiece of this PR. - `lib/filters.ts` (delimiter encoding) and `app/sites/[id]/page.tsx` line 232 (`prevStats` missing filters) need attention before this is safe to ship. <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | lib/filters.ts | New filter serialization utility — delimiter collision bug: `|` and `,` used as separators are not escaped, breaking parse for referrer URLs, page paths with query strings, and city names containing commas. | | app/sites/[id]/page.tsx | Main dashboard page extended with filters, event properties, and scroll depth panels; `prevStats` fetched without active filters causes misleading KPI period-over-period comparisons when filters are applied. | | public/script.js | Added 404 detection, scroll depth, outbound link, file download, and custom event props tracking — well-guarded with opt-out attributes; outbound/download events omit destination URL props, and `'404'` event name is unconventional. | | components/dashboard/EventProperties.tsx | New component for per-event property breakdown; property-values fetch lacks a loading indicator and error handler, which can leave stale data visible on key switch failure. | | components/dashboard/AddFilterDropdown.tsx | New two-step filter dropdown with async suggestion fetching, cancellation via cleanup flag, keyboard and outside-click handling — well-structured with no issues found. | | lib/swr/dashboard.ts | All SWR hooks correctly updated to accept and thread `filters` into both the cache key and fetcher — cache invalidation on filter change is handled properly. | | lib/api/stats.ts | Uniform `filters` parameter added to all private dashboard API functions; new event-property endpoints are well-typed and follow existing patterns. | </details> </details> <h3>Flowchart</h3> ```mermaid %%{init: {'theme': 'neutral'}}%% flowchart TD User([User]) -->|clicks item / uses Filter dropdown| AddFilterDropdown AddFilterDropdown -->|onFetchSuggestions| API[(API\ngetTopPages / getCountries / etc.)] API -->|FilterSuggestion list| AddFilterDropdown AddFilterDropdown -->|DimensionFilter| handleAddFilter handleAddFilter -->|dedup check| filters[(filters state)] filters -->|serializeFilters| filtersParam[filtersParam\nstring] filtersParam -->|URL sync\nreplaceState| URL[/URL ?filters=.../] filtersParam -->|SWR cache key| SWRHooks SWRHooks -->|filters query param| BackendAPI[(Backend API)] BackendAPI -->|filtered data| SWRHooks SWRHooks -->|data| DashComponents[Dashboard Panels\nPages · Referrers · Locations\nTech · Goals · Chart] DashComponents -->|onFilter| handleAddFilter FilterBar -->|onRemove / onClear| filters filters --> FilterBar URL -->|parseFiltersFromURL on init| filters ``` <!-- greptile_other_comments_section --> <sub>Last reviewed commit: 8ebd8ba</sub>
greptile-apps[bot] (Migrated from github.com) reviewed 2026-03-07 00:21:21 +00:00
greptile-apps[bot] (Migrated from github.com) commented 2026-03-07 00:21:17 +00:00

Previous-period stats comparison ignores active filters

prevStats is fetched without the active filtersParam, so when a user filters the dashboard (e.g. "country is US"), the KPI cards compare filtered current-period data against unfiltered previous-period data. This produces misleading percentage deltas.

useStats already accepts a filters parameter — pass it here:

  const { data: prevStats } = useStats(siteId, prevRange.start, prevRange.end, filtersParam || undefined)
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/sites/[id]/page.tsx
Line: 232

Comment:
**Previous-period stats comparison ignores active filters**

`prevStats` is fetched without the active `filtersParam`, so when a user filters the dashboard (e.g. "country is US"), the KPI cards compare filtered current-period data against *unfiltered* previous-period data. This produces misleading percentage deltas.

`useStats` already accepts a `filters` parameter — pass it here:

```suggestion
  const { data: prevStats } = useStats(siteId, prevRange.start, prevRange.end, filtersParam || undefined)
```

How can I resolve this? If you propose a fix, please make it concise.
**Previous-period stats comparison ignores active filters** `prevStats` is fetched without the active `filtersParam`, so when a user filters the dashboard (e.g. "country is US"), the KPI cards compare filtered current-period data against *unfiltered* previous-period data. This produces misleading percentage deltas. `useStats` already accepts a `filters` parameter — pass it here: ```suggestion const { data: prevStats } = useStats(siteId, prevRange.start, prevRange.end, filtersParam || undefined) ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: app/sites/[id]/page.tsx Line: 232 Comment: **Previous-period stats comparison ignores active filters** `prevStats` is fetched without the active `filtersParam`, so when a user filters the dashboard (e.g. "country is US"), the KPI cards compare filtered current-period data against *unfiltered* previous-period data. This produces misleading percentage deltas. `useStats` already accepts a `filters` parameter — pass it here: ```suggestion const { data: prevStats } = useStats(siteId, prevRange.start, prevRange.end, filtersParam || undefined) ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
@@ -0,0 +31,4 @@
if (!selectedKey) return
getEventPropertyValues(siteId, eventName, selectedKey, dateRange.start, dateRange.end)
.then(setValues)
}, [siteId, eventName, selectedKey, dateRange.start, dateRange.end])
greptile-apps[bot] (Migrated from github.com) commented 2026-03-07 00:21:18 +00:00

Missing loading state and error handling for property values fetch

When selectedKey changes (user taps a different property pill), values is updated asynchronously but there is no loading indicator and no .catch(). If the API request fails, values silently retains the data from the previous key selection, showing stale data without any indication of the error.

Consider adding a valuesLoading flag and a .catch() similar to how the keys fetch is handled:

useEffect(() => {
  if (!selectedKey) return
  setValuesLoading(true)
  getEventPropertyValues(siteId, eventName, selectedKey, dateRange.start, dateRange.end)
    .then(setValues)
    .catch(() => setValues([]))
    .finally(() => setValuesLoading(false))
}, [siteId, eventName, selectedKey, dateRange.start, dateRange.end])
Prompt To Fix With AI
This is a comment left during a code review.
Path: components/dashboard/EventProperties.tsx
Line: 30-34

Comment:
**Missing loading state and error handling for property values fetch**

When `selectedKey` changes (user taps a different property pill), `values` is updated asynchronously but there is no loading indicator and no `.catch()`. If the API request fails, `values` silently retains the data from the previous key selection, showing stale data without any indication of the error.

Consider adding a `valuesLoading` flag and a `.catch()` similar to how the keys fetch is handled:

```ts
useEffect(() => {
  if (!selectedKey) return
  setValuesLoading(true)
  getEventPropertyValues(siteId, eventName, selectedKey, dateRange.start, dateRange.end)
    .then(setValues)
    .catch(() => setValues([]))
    .finally(() => setValuesLoading(false))
}, [siteId, eventName, selectedKey, dateRange.start, dateRange.end])
```

How can I resolve this? If you propose a fix, please make it concise.
**Missing loading state and error handling for property values fetch** When `selectedKey` changes (user taps a different property pill), `values` is updated asynchronously but there is no loading indicator and no `.catch()`. If the API request fails, `values` silently retains the data from the previous key selection, showing stale data without any indication of the error. Consider adding a `valuesLoading` flag and a `.catch()` similar to how the keys fetch is handled: ```ts useEffect(() => { if (!selectedKey) return setValuesLoading(true) getEventPropertyValues(siteId, eventName, selectedKey, dateRange.start, dateRange.end) .then(setValues) .catch(() => setValues([])) .finally(() => setValuesLoading(false)) }, [siteId, eventName, selectedKey, dateRange.start, dateRange.end]) ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/dashboard/EventProperties.tsx Line: 30-34 Comment: **Missing loading state and error handling for property values fetch** When `selectedKey` changes (user taps a different property pill), `values` is updated asynchronously but there is no loading indicator and no `.catch()`. If the API request fails, `values` silently retains the data from the previous key selection, showing stale data without any indication of the error. Consider adding a `valuesLoading` flag and a `.catch()` similar to how the keys fetch is handled: ```ts useEffect(() => { if (!selectedKey) return setValuesLoading(true) getEventPropertyValues(siteId, eventName, selectedKey, dateRange.start, dateRange.end) .then(setValues) .catch(() => setValues([])) .finally(() => setValuesLoading(false)) }, [siteId, eventName, selectedKey, dateRange.start, dateRange.end]) ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
@@ -0,0 +48,4 @@
operator: operator as DimensionFilter['operator'],
values: valuesRaw?.split(';') ?? [],
}
}).filter(f => f.dimension && f.operator && f.values.length > 0)
greptile-apps[bot] (Migrated from github.com) commented 2026-03-07 00:21:16 +00:00

Delimiter collision breaks filter parsing for URLs and city names

The serialization format uses | as an intra-filter separator and , as an inter-filter separator, but neither character is escaped. This causes parseFiltersFromURL to silently drop data for any filter value containing these characters:

  • Referrer values (e.g. https://t.co/abc|xyz) — after part.split('|'), destructuring assigns valuesRaw = 'https://t.co/abc' and the xyz portion becomes an ignored 4th element.
  • Page paths or city names with commas (e.g. Portland, Oregon or /shop?a=1,b=2) — raw.split(',') splits the whole filter string first, so Portland, Oregon becomes two separate, unparseable fragments.

Consider URL-encoding the individual dimension/value parts before joining, or using a safer delimiter (e.g. encodeURIComponent on values):

// serialize
.map(f => `${f.dimension}|${f.operator}|${f.values.map(encodeURIComponent).join(';')}`)
.join(',')

// parse valuesRaw
values: valuesRaw?.split(';').map(decodeURIComponent) ?? [],
Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/filters.ts
Line: 34-51

Comment:
**Delimiter collision breaks filter parsing for URLs and city names**

The serialization format uses `|` as an intra-filter separator and `,` as an inter-filter separator, but neither character is escaped. This causes `parseFiltersFromURL` to silently drop data for any filter value containing these characters:

- **Referrer values** (e.g. `https://t.co/abc|xyz`) — after `part.split('|')`, destructuring assigns `valuesRaw = 'https://t.co/abc'` and the `xyz` portion becomes an ignored 4th element.
- **Page paths or city names with commas** (e.g. `Portland, Oregon` or `/shop?a=1,b=2`) — `raw.split(',')` splits the whole filter string first, so `Portland, Oregon` becomes two separate, unparseable fragments.

Consider URL-encoding the individual dimension/value parts before joining, or using a safer delimiter (e.g. `encodeURIComponent` on values):

```ts
// serialize
.map(f => `${f.dimension}|${f.operator}|${f.values.map(encodeURIComponent).join(';')}`)
.join(',')

// parse valuesRaw
values: valuesRaw?.split(';').map(decodeURIComponent) ?? [],
```

How can I resolve this? If you propose a fix, please make it concise.
**Delimiter collision breaks filter parsing for URLs and city names** The serialization format uses `|` as an intra-filter separator and `,` as an inter-filter separator, but neither character is escaped. This causes `parseFiltersFromURL` to silently drop data for any filter value containing these characters: - **Referrer values** (e.g. `https://t.co/abc|xyz`) — after `part.split('|')`, destructuring assigns `valuesRaw = 'https://t.co/abc'` and the `xyz` portion becomes an ignored 4th element. - **Page paths or city names with commas** (e.g. `Portland, Oregon` or `/shop?a=1,b=2`) — `raw.split(',')` splits the whole filter string first, so `Portland, Oregon` becomes two separate, unparseable fragments. Consider URL-encoding the individual dimension/value parts before joining, or using a safer delimiter (e.g. `encodeURIComponent` on values): ```ts // serialize .map(f => `${f.dimension}|${f.operator}|${f.values.map(encodeURIComponent).join(';')}`) .join(',') // parse valuesRaw values: valuesRaw?.split(';').map(decodeURIComponent) ?? [], ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: lib/filters.ts Line: 34-51 Comment: **Delimiter collision breaks filter parsing for URLs and city names** The serialization format uses `|` as an intra-filter separator and `,` as an inter-filter separator, but neither character is escaped. This causes `parseFiltersFromURL` to silently drop data for any filter value containing these characters: - **Referrer values** (e.g. `https://t.co/abc|xyz`) — after `part.split('|')`, destructuring assigns `valuesRaw = 'https://t.co/abc'` and the `xyz` portion becomes an ignored 4th element. - **Page paths or city names with commas** (e.g. `Portland, Oregon` or `/shop?a=1,b=2`) — `raw.split(',')` splits the whole filter string first, so `Portland, Oregon` becomes two separate, unparseable fragments. Consider URL-encoding the individual dimension/value parts before joining, or using a safer delimiter (e.g. `encodeURIComponent` on values): ```ts // serialize .map(f => `${f.dimension}|${f.operator}|${f.values.map(encodeURIComponent).join(';')}`) .join(',') // parse valuesRaw values: valuesRaw?.split(';').map(decodeURIComponent) ?? [], ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
@@ -349,0 +434,4 @@
document.addEventListener('click', function(e) {
var el = e.target;
// * Walk up from clicked element to find nearest <a> tag
greptile-apps[bot] (Migrated from github.com) commented 2026-03-07 00:21:20 +00:00

'404' event name is an invalid identifier per the script's own validation

trackCustomEvent validates event names with EVENT_NAME_REGEX = /^[a-zA-Z0-9_]+$/ — digits are technically allowed, so '404' passes. However, a name that starts with a digit can be ambiguous and is inconsistent with the documented convention ("letters, numbers, underscores"). More importantly, when the event name is displayed in the dashboard it is converted with .replace(/_/g, ' '), yielding the display string "404" — which is usable, but a name like 'page_not_found' would display more clearly as "page not found" and remain consistent with the naming convention shown in the docs embed (pulse.track('event_name')).

      trackCustomEvent('page_not_found');
Prompt To Fix With AI
This is a comment left during a code review.
Path: public/script.js
Line: 377-385

Comment:
**`'404'` event name is an invalid identifier per the script's own validation**

`trackCustomEvent` validates event names with `EVENT_NAME_REGEX = /^[a-zA-Z0-9_]+$/` — digits are technically allowed, so `'404'` passes. However, a name that starts with a digit can be ambiguous and is inconsistent with the documented convention ("letters, numbers, underscores"). More importantly, when the event name is displayed in the dashboard it is converted with `.replace(/_/g, ' ')`, yielding the display string `"404"` — which is usable, but a name like `'page_not_found'` would display more clearly as "page not found" and remain consistent with the naming convention shown in the docs embed (`pulse.track('event_name')`).

```suggestion
      trackCustomEvent('page_not_found');
```

How can I resolve this? If you propose a fix, please make it concise.
**`'404'` event name is an invalid identifier per the script's own validation** `trackCustomEvent` validates event names with `EVENT_NAME_REGEX = /^[a-zA-Z0-9_]+$/` — digits are technically allowed, so `'404'` passes. However, a name that starts with a digit can be ambiguous and is inconsistent with the documented convention ("letters, numbers, underscores"). More importantly, when the event name is displayed in the dashboard it is converted with `.replace(/_/g, ' ')`, yielding the display string `"404"` — which is usable, but a name like `'page_not_found'` would display more clearly as "page not found" and remain consistent with the naming convention shown in the docs embed (`pulse.track('event_name')`). ```suggestion trackCustomEvent('page_not_found'); ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 377-385 Comment: **`'404'` event name is an invalid identifier per the script's own validation** `trackCustomEvent` validates event names with `EVENT_NAME_REGEX = /^[a-zA-Z0-9_]+$/` — digits are technically allowed, so `'404'` passes. However, a name that starts with a digit can be ambiguous and is inconsistent with the documented convention ("letters, numbers, underscores"). More importantly, when the event name is displayed in the dashboard it is converted with `.replace(/_/g, ' ')`, yielding the display string `"404"` — which is usable, but a name like `'page_not_found'` would display more clearly as "page not found" and remain consistent with the naming convention shown in the docs embed (`pulse.track('event_name')`). ```suggestion trackCustomEvent('page_not_found'); ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
greptile-apps[bot] (Migrated from github.com) commented 2026-03-07 00:21:19 +00:00

Outbound link and file download events omit the destination URL

trackCustomEvent('outbound_link') and trackCustomEvent('file_download') fire without any props, so the backend only knows that a click happened on the current page — not where the user went. The new props parameter added in this PR was specifically designed for this purpose.

Consider passing the destination as a property:

// file download
trackCustomEvent('file_download', { url: url.href });
return;

// outbound link
trackCustomEvent('outbound_link', { url: url.href });

This gives users actionable data ("which external links are clicked most") rather than a raw event count.

Prompt To Fix With AI
This is a comment left during a code review.
Path: public/script.js
Line: 447-455

Comment:
**Outbound link and file download events omit the destination URL**

`trackCustomEvent('outbound_link')` and `trackCustomEvent('file_download')` fire without any `props`, so the backend only knows *that* a click happened on the current page — not *where* the user went. The new `props` parameter added in this PR was specifically designed for this purpose.

Consider passing the destination as a property:

```js
// file download
trackCustomEvent('file_download', { url: url.href });
return;

// outbound link
trackCustomEvent('outbound_link', { url: url.href });
```

This gives users actionable data ("which external links are clicked most") rather than a raw event count.

How can I resolve this? If you propose a fix, please make it concise.
**Outbound link and file download events omit the destination URL** `trackCustomEvent('outbound_link')` and `trackCustomEvent('file_download')` fire without any `props`, so the backend only knows *that* a click happened on the current page — not *where* the user went. The new `props` parameter added in this PR was specifically designed for this purpose. Consider passing the destination as a property: ```js // file download trackCustomEvent('file_download', { url: url.href }); return; // outbound link trackCustomEvent('outbound_link', { url: url.href }); ``` This gives users actionable data ("which external links are clicked most") rather than a raw event count. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 447-455 Comment: **Outbound link and file download events omit the destination URL** `trackCustomEvent('outbound_link')` and `trackCustomEvent('file_download')` fire without any `props`, so the backend only knows *that* a click happened on the current page — not *where* the user went. The new `props` parameter added in this PR was specifically designed for this purpose. Consider passing the destination as a property: ```js // file download trackCustomEvent('file_download', { url: url.href }); return; // outbound link trackCustomEvent('outbound_link', { url: url.href }); ``` This gives users actionable data ("which external links are clicked most") rather than a raw event count. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
Sign in to join this conversation.
No description provided.