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

web: report analytics on sidebar name filter #4403

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 45 additions & 0 deletions web/src/OverviewSidebarOptions.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { mount, ReactWrapper } from "enzyme"
import fetchMock from "jest-fetch-mock"
import React from "react"
import { MemoryRouter } from "react-router"
import { expectIncrs } from "./analytics_test_helpers"
import { accessorsForTesting, tiltfileKeyContext } from "./LocalStorage"
import {
TestsWithErrors,
Expand All @@ -9,6 +11,7 @@ import {
} from "./OverviewResourceSidebar.stories"
import {
AlertsOnTopToggle,
ClearResourceNameFilterButton,
FilterOptionList,
OverviewSidebarOptions,
ResourceNameFilterTextField,
Expand Down Expand Up @@ -76,8 +79,16 @@ function clickTestsOnlyControl(root: ReactWrapper) {
const allNames = ["(Tiltfile)", "vigoda", "snack", "beep", "boop"]

describe("overview sidebar options", () => {
beforeEach(() => {
fetchMock.resetMocks()
fetchMock.mockResponse(JSON.stringify({}))
jest.useFakeTimers()
})

afterEach(() => {
fetchMock.resetMocks()
localStorage.clear()
jest.useRealTimers()
})

it("shows all resources by default", () => {
Expand Down Expand Up @@ -250,6 +261,40 @@ describe("overview sidebar options", () => {
"No matching resources",
])
})

it("reports analytics, debounced, when search bar edited", () => {
const root = mount(
<OverviewSidebarOptions
options={defaultOptions}
setOptions={() => {}}
showFilters={true}
/>
)
const tf = root.find(ResourceNameFilterTextField)
// two changes in rapid succession should result in only one analytics event
tf.props().onChange({ target: { value: "foo" } })
tf.props().onChange({ target: { value: "foobar" } })
expectIncrs(...[])
jest.runTimersToTime(10000)
expectIncrs({ name: "ui.web.resourceNameFilter", tags: { action: "edit" } })
})

it("reports analytics when search bar cleared", () => {
const root = mount(
<OverviewSidebarOptions
options={{ ...defaultOptions, resourceNameFilter: "foo" }}
setOptions={() => {}}
showFilters={true}
/>
)
const button = root.find(ClearResourceNameFilterButton)

button.simulate("click")
expectIncrs({
name: "ui.web.resourceNameFilter",
tags: { action: "clear" },
})
})
})

it("toggles/untoggles Alerts On Top sorting when button clicked", () => {
Expand Down
37 changes: 30 additions & 7 deletions web/src/OverviewSidebarOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { InputAdornment, TextField } from "@material-ui/core"
import { debounce, InputAdornment, TextField } from "@material-ui/core"
import { InputProps as StandardInputProps } from "@material-ui/core/Input/Input"
import React, { Dispatch, SetStateAction } from "react"
import styled from "styled-components"
import { incr } from "./analytics"
import { ReactComponent as CloseSvg } from "./assets/svg/close.svg"
import { ReactComponent as SearchSvg } from "./assets/svg/search.svg"
import {
Expand Down Expand Up @@ -118,6 +119,12 @@ export const ResourceNameFilterTextField = styled(TextField)`
}
`

export const ClearResourceNameFilterButton = styled.button`
${mixinResetButtonStyle};
display: flex;
align-items: center;
`

type OverviewSidebarOptionsProps = {
showFilters: boolean
options: SidebarOptions
Expand Down Expand Up @@ -191,31 +198,47 @@ function filterOptions(props: OverviewSidebarOptionsProps) {
)
}

const resourceNameFilterEventName = "ui.web.resourceNameFilter"

// debounce so we don't send for every single keypress
let incrResourceNameFilterEdit = debounce(() => {
incr("ui.web.resourceNameFilter", { action: "edit" })
}, 5000)

function ResourceNameFilter(props: OverviewSidebarOptionsProps) {
let inputProps: Partial<StandardInputProps> = {
startAdornment: (
<InputAdornment position="start">
<SearchSvg style={{ fill: Color.grayLightest }} />
<SearchSvg fill={Color.grayLightest} />
</InputAdornment>
),
}

// only show the "x" to clear if there's any input to clear
if (props.options.resourceNameFilter) {
const onClearClick = () => {
incr(resourceNameFilterEventName, { action: "clear" })
setResourceNameFilter("", props)
}

inputProps.endAdornment = (
<InputAdornment position="end">
<CloseSvg
style={{ fill: Color.grayLightest, cursor: "pointer" }}
onClick={() => setResourceNameFilter("", props)}
/>
<ClearResourceNameFilterButton onClick={onClearClick}>
<CloseSvg fill={Color.grayLightest} />
</ClearResourceNameFilterButton>
</InputAdornment>
)
}

const onChange = (e: any) => {
incrResourceNameFilterEdit()
setResourceNameFilter(e.target.value, props)
}

return (
<ResourceNameFilterTextField
value={props.options.resourceNameFilter ?? ""}
onChange={(e) => setResourceNameFilter(e.target.value, props)}
onChange={onChange}
placeholder="Filter resources by name"
InputProps={inputProps}
variant="outlined"
Expand Down
14 changes: 14 additions & 0 deletions web/src/analytics_test_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,17 @@ export function expectIncr(fetchMockIndex: number, name: string, tags: Tags) {
])
)
}

export function expectIncrs(...incrs: { name: string; tags: Tags }[]) {
const expectedRequestBodies = incrs.map((i) =>
JSON.stringify([
{
verb: "incr",
name: i.name,
tags: i.tags,
},
])
)
const actualRequestBodies = fetchMock.mock.calls.map((e) => e[1]?.body)
expect(actualRequestBodies).toEqual(expectedRequestBodies)
}