Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Please ignore: Preview environment for has_done/has_not_done filters #4995

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ff9a8e9
Expose site_id and site_native_stats_start_at via query
macobo Jan 13, 2025
37784a8
Very basic has_done/has_done_not operator support
macobo Jan 13, 2025
efced25
Add validations that only event: dimensions can be used within has_do…
macobo Jan 13, 2025
4109b4e
Allow event:goal filters nested within has_done/has_done_not behavior…
macobo Jan 13, 2025
aa6eeee
Minor fix for do_decide_custom_prop_table
macobo Jan 13, 2025
871c76e
has_done support for goals
macobo Jan 14, 2025
675ddc9
Dont query imports when behavioral filters are present
macobo Jan 14, 2025
cbf7b32
Update callsites of filtering_on_dimension? to work with new behavior…
macobo Jan 14, 2025
72a339e
has_done_not -> has_not_done
macobo Jan 16, 2025
d6155ea
Changelog entry
macobo Jan 16, 2025
5170a80
Typegen
macobo Jan 16, 2025
36420ac
credo cleanup
macobo Jan 16, 2025
bc314ef
Fix changelog
macobo Jan 16, 2025
83aa4cb
Remove changelog
macobo Jan 20, 2025
ced32da
Mark has_done as internal-only
macobo Jan 20, 2025
59ec12d
combine two validations into a single loop
macobo Jan 20, 2025
02e274d
has_done is now session-based not user-based
macobo Jan 20, 2025
6d57cfe
Update a test
macobo Jan 20, 2025
a52faa6
Simple frontend for has_done_not
macobo Jan 14, 2025
de8976b
Simple UI for goal filter adding or removal
macobo Jan 16, 2025
cff2739
Better alignment on trash icons, avoid moving around if row expands
macobo Jan 16, 2025
204565e
Refactor filter text functions, share code
macobo Jan 16, 2025
2d20918
has_not_done, special casing for has not done when formatting filter …
macobo Jan 16, 2025
2333399
Changelog
macobo Jan 16, 2025
1f0f4b8
Fix lint
macobo Jan 16, 2025
14dd9e3
prettier format
macobo Jan 16, 2025
5842468
Add tests
macobo Jan 16, 2025
97d00b8
Lowercase Goal
macobo Jan 16, 2025
1f9f34a
Update changelog
macobo Jan 20, 2025
e39a7ee
has_not_done for goals is now named `is not` in the UI
macobo Jan 20, 2025
077bf37
prettier
macobo Jan 20, 2025
2b2d4ee
Document and test serializeApiFilters
macobo Jan 21, 2025
fb6727b
has_done/has_not_done filters for most events
macobo Jan 20, 2025
4cb737d
Show add row for all modals
macobo Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ All notable changes to this project will be documented in this file.
- Dashboard shows comparisons for all reports
- UTM Medium report and API shows (gclid) and (msclkid) for paid searches when no explicit utm medium present.
- Support for `case_sensitive: false` modifiers in Stats API V2 filters for case-insensitive searches.
- Add filter `is not` for goals in dashboard plausible/analytics#4983

### Removed

- Internal stats API routes no longer support legacy dashboard filter format.

### Changed

- Filters appear in the search bar as ?f=is,page,/docs,/blog&f=... instead of ?filters=((is,page,(/docs,/blog)),...) for Plausible links sent on various platforms to work reliably.
- Filters appear in the search bar as ?f=is,page,/docs,/blog&f=... instead of ?filters=((is,page,(/docs,/blog)),...) for Plausible links sent on various platforms to work reliably.
- Details modal search inputs are now case-insensitive.
- Improved report performance in cases where site has a lot of unique pathnames

Expand Down
11 changes: 10 additions & 1 deletion assets/js/dashboard/components/filter-operator-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
FILTER_OPERATIONS,
FILTER_OPERATIONS_DISPLAY_NAMES,
supportsContains,
supportsIsNot
supportsIsNot,
supportsHasDone
} from '../util/filters'
import { Menu, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
Expand Down Expand Up @@ -75,6 +76,14 @@ export default function FilterOperatorSelector(props) {
FILTER_OPERATIONS.isNot,
supportsIsNot(filterName)
)}
{renderTypeItem(
FILTER_OPERATIONS.has_done,
supportsHasDone(filterName)
)}
{renderTypeItem(
FILTER_OPERATIONS.has_not_done,
supportsHasDone(filterName)
)}
{renderTypeItem(
FILTER_OPERATIONS.contains,
supportsContains(filterName)
Expand Down
7 changes: 3 additions & 4 deletions assets/js/dashboard/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import {
cleanLabels,
FILTER_MODAL_TO_FILTER_GROUP,
formatFilterGroup,
EVENT_PROPS_PREFIX,
plainFilterText,
styledFilterText
} from "./util/filters";
EVENT_PROPS_PREFIX
} from "./util/filters"
import { plainFilterText, styledFilterText } from "./util/filter-text"

const WRAPSTATE = { unwrapped: 0, waiting: 1, wrapped: 2 }

Expand Down
5 changes: 2 additions & 3 deletions assets/js/dashboard/nav-menu/filter-pills-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import { FilterPill } from './filter-pill'
import {
cleanLabels,
EVENT_PROPS_PREFIX,
FILTER_GROUP_TO_MODAL_TYPE,
plainFilterText,
styledFilterText
FILTER_GROUP_TO_MODAL_TYPE
} from '../util/filters'
import { styledFilterText, plainFilterText } from '../util/filter-text'
import { useAppNavigate } from '../navigation/use-app-navigate'
import classNames from 'classnames'

Expand Down
10 changes: 5 additions & 5 deletions assets/js/dashboard/nav-menu/filters-bar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ test('user can see expected filters and clear them one by one or all together',
)

expect(queryFilterPills().map((m) => m.textContent)).toEqual([
'Country is Germany ',
'Goal is Subscribed to Newsletter ',
'Page is /docs or /blog '
'Country is Germany',
'Goal is Subscribed to Newsletter',
'Page is /docs or /blog'
])

await userEvent.click(
Expand All @@ -74,8 +74,8 @@ test('user can see expected filters and clear them one by one or all together',
)

expect(queryFilterPills().map((m) => m.textContent)).toEqual([
'Goal is Subscribed to Newsletter ',
'Page is /docs or /blog '
'Goal is Subscribed to Newsletter',
'Page is /docs or /blog'
])

await userEvent.click(
Expand Down
5 changes: 4 additions & 1 deletion assets/js/dashboard/stats/modals/filter-modal-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function FilterModalGroup({
[filterGroup, rows]
)

const showAddRow = filterGroup == 'props'
const showAddRow = true // ['props', 'goal'].includes(filterGroup)
const showTitle = filterGroup != 'props'

return (
Expand All @@ -42,7 +42,10 @@ export default function FilterModalGroup({
key={id}
filter={filter}
labels={labels}
canDelete={showAddRow}
showDelete={rows.length > 1}
onUpdate={(newFilter, labelUpdate) => onUpdateRowValue(id, newFilter, labelUpdate)}
onDelete={() => onDeleteRow(id)}
/>
)
)}
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/stats/modals/filter-modal-props-row.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export default function FilterModalPropsRow({
/>
</div>
{showDelete && (
<div className="col-span-1 flex flex-col justify-center">
<div className="col-span-1 flex flex-col mt-2">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a
className="ml-2 text-red-600 h-5 w-5 cursor-pointer"
Expand Down
29 changes: 27 additions & 2 deletions assets/js/dashboard/stats/modals/filter-modal-row.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/** @format */

import React, { useMemo } from 'react'
import { TrashIcon } from '@heroicons/react/20/solid'
import classNames from 'classnames'

import FilterOperatorSelector from '../../components/filter-operator-selector'
import Combobox from '../../components/combobox'
Expand All @@ -16,7 +18,14 @@ import { apiPath } from '../../util/url'
import { useQueryContext } from '../../query-context'
import { useSiteContext } from '../../site-context'

export default function FilterModalRow({ filter, labels, onUpdate }) {
export default function FilterModalRow({
filter,
labels,
canDelete,
showDelete,
onUpdate,
onDelete
}) {
const { query } = useQueryContext()
const site = useSiteContext()
const [operation, filterKey, clauses] = filter
Expand Down Expand Up @@ -64,7 +73,12 @@ export default function FilterModalRow({ filter, labels, onUpdate }) {
}

return (
<div className="grid grid-cols-11 mt-1">
<div
className={classNames('grid mt-1', {
'grid-cols-12': canDelete,
'grid-cols-11': !canDelete
})}
>
<div className="col-span-3">
<FilterOperatorSelector
forFilter={filterKey}
Expand All @@ -83,6 +97,17 @@ export default function FilterModalRow({ filter, labels, onUpdate }) {
placeholder={`Select ${withIndefiniteArticle(formattedFilters[filterKey])}`}
/>
</div>
{showDelete && (
<div className="col-span-1 flex flex-col mt-2">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a
className="ml-2 text-red-600 h-5 w-5 cursor-pointer"
onClick={onDelete}
>
<TrashIcon />
</a>
</div>
)}
</div>
)
}
Expand Down
4 changes: 2 additions & 2 deletions assets/js/dashboard/stats/reports/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import {
cleanLabels,
replaceFilterByPrefix,
isRealTimeDashboard,
hasGoalFilter,
plainFilterText
hasGoalFilter
} from '../../util/filters'
import { plainFilterText } from '../../util/filter-text'
import { useQueryContext } from '../../query-context'

const MAX_ITEMS = 9
Expand Down
24 changes: 24 additions & 0 deletions assets/js/dashboard/util/filter-text.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import { DashboardQuery, Filter, FilterClauseLabels } from '../query'
import { plainFilterText, styledFilterText } from './filter-text'
import { render, screen } from '@testing-library/react'

describe('styledFilterText() and plainFilterText()', () => {
it.each<[Filter, FilterClauseLabels, string]>([
[['is', 'page', ['/docs', '/blog']], {}, 'Page is /docs or /blog'],
[['is', 'country', ['US']], { US: 'United States' }, 'Country is United States'],
[['is', 'goal', ['Signup']], {}, 'Goal is Signup'],
[['is', 'props:browser_language', ['en-US']], {}, 'Property browser_language is en-US'],
[['has_not_done', 'goal', ['Signup', 'Login']], {}, 'Goal is not Signup or Login'],
])(
'when filter is %p and labels are %p, functions return %p',
(filter, labels, expectedPlainText) => {
const query = { labels } as unknown as DashboardQuery

expect(plainFilterText(query, filter)).toBe(expectedPlainText)

render(<p data-testid="filter-text">{styledFilterText(query, filter)}</p>)
expect(screen.getByTestId('filter-text')).toHaveTextContent(expectedPlainText)
}
)
})
77 changes: 77 additions & 0 deletions assets/js/dashboard/util/filter-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* @format */

import React, { ReactNode, isValidElement, Fragment } from 'react'
import { DashboardQuery, Filter } from '../query'
import {
EVENT_PROPS_PREFIX,
FILTER_OPERATIONS_DISPLAY_NAMES,
formattedFilters,
getLabel,
getPropertyKeyFromFilterKey
} from './filters'

export function styledFilterText(
query: DashboardQuery,
[operation, filterKey, clauses]: Filter
) {
if (filterKey.startsWith(EVENT_PROPS_PREFIX)) {
const propKey = getPropertyKeyFromFilterKey(filterKey)
return (
<>
Property <b>{propKey}</b> {FILTER_OPERATIONS_DISPLAY_NAMES[operation]}{' '}
{formatClauses(clauses)}
</>
)
}

const formattedFilter = (
formattedFilters as Record<string, string | undefined>
)[filterKey]
const clausesLabels = clauses.map((value) =>
getLabel(query.labels, filterKey, value)
)

if (!formattedFilter) {
throw new Error(`Unknown filter: ${filterKey}`)
}

return (
<>
{capitalize(formattedFilter)} {FILTER_OPERATIONS_DISPLAY_NAMES[operation]}{' '}
{formatClauses(clausesLabels)}
</>
)
}

export function plainFilterText(query: DashboardQuery, filter: Filter) {
return reactNodeToString(styledFilterText(query, filter))
}

function formatClauses(labels: Array<string | number>): ReactNode[] {
return labels.map((label, index) => (
<Fragment key={index}>
{index > 0 && ' or '}
<b>{label}</b>
</Fragment>
))
}

function capitalize(str: string): string {
return str[0].toUpperCase() + str.slice(1)
}

function reactNodeToString(reactNode: ReactNode): string {
let string = ''
if (typeof reactNode === 'string') {
string = reactNode
} else if (typeof reactNode === 'number') {
string = reactNode.toString()
} else if (reactNode instanceof Array) {
reactNode.forEach(function (child) {
string += reactNodeToString(child)
})
} else if (isValidElement(reactNode)) {
string += reactNodeToString(reactNode.props.children)
}
return string
}
Loading
Loading