diff --git a/src/app/WardrobePage/SearchToolbar.js b/src/app/WardrobePage/SearchToolbar.js index c3094a3..ac0fc17 100644 --- a/src/app/WardrobePage/SearchToolbar.js +++ b/src/app/WardrobePage/SearchToolbar.js @@ -22,6 +22,7 @@ import { ClassNames } from "@emotion/react"; import Autosuggest from "react-autosuggest"; import useCurrentUser from "../components/useCurrentUser"; +import { logAndCapture } from "../util"; export const emptySearchQuery = { value: "", @@ -192,12 +193,9 @@ function SearchToolbar({ } }} onSuggestionSelected={(e, { suggestion }) => { - const valueWithoutLastWord = query.value - ? query.value.match(/^(.*?)\s*\S+$/)[1] - : query.value; onChange({ ...query, - value: valueWithoutLastWord, + value: removeLastWord(query.value), filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel, filterToItemKind: suggestion.itemKind || query.filterToItemKind, filterToCurrentUserOwnsOrWants: @@ -425,4 +423,27 @@ function pluralizeZoneLabel(zoneLabel) { } } +/** + * removeLastWord returns a copy of the text, with the last word and any + * preceding space removed. + */ +function removeLastWord(text) { + // This regex matches the full text, and assigns the last word and any + // preceding text to subgroup 2, and all preceding text to subgroup 1. If + // there's no last word, we'll still match, and the full string will be in + // subgroup 1, including any space - no changes made! + const match = text.match(/^(.*?)(\s*\S+)?$/); + if (!match) { + logAndCapture( + new Error( + `Assertion failure: pattern should match any input text, ` + + `but failed to match ${JSON.stringify(text)}` + ) + ); + return text; + } + + return match[1]; +} + export default SearchToolbar; diff --git a/src/app/util.js b/src/app/util.js index 5a65e09..adbc144 100644 --- a/src/app/util.js +++ b/src/app/util.js @@ -1,6 +1,7 @@ import React from "react"; import { Box, Heading, useColorModeValue } from "@chakra-ui/react"; import loadableLibrary from "@loadable/component"; +import * as Sentry from "@sentry/react"; /** * Delay hides its content at first, then shows it after the given delay. @@ -317,3 +318,14 @@ export function loadable(load, options) { options ); } + +/** + * logAndCapture will print an error to the console, and send it to Sentry. + * + * This is useful when there's a graceful recovery path, but it's still a + * genuinely unexpected error worth logging. + */ +export function logAndCapture(e) { + console.error(e); + Sentry.captureException(e); +}