From 9a61ec63d8b7746a618a47af21c420f5cef3460e Mon Sep 17 00:00:00 2001 From: Mohamed Khelif Date: Wed, 14 Aug 2024 16:00:18 -0400 Subject: [PATCH] DEVPROD-8659 Move analytics utils to shared lib directory (#301) --- README.md | 12 +- apps/parsley/src/analytics/addPageAction.ts | 57 ---------- .../loadingPage/useLogDownloadAnalytics.ts | 5 +- .../analytics/logDrop/useLogDropAnalytics.ts | 6 +- .../logWindow/useLogWindowAnalytics.ts | 5 +- .../preferences/usePreferencesAnalytics.ts | 5 +- .../shortcuts/useShortcutAnalytics.ts | 6 +- apps/parsley/src/analytics/types.ts | 6 + .../parsley/src/analytics/useAnalyticsRoot.ts | 23 ---- .../src/constants/externalURLTemplates.ts | 2 +- apps/parsley/src/constants/logURLTemplates.ts | 2 +- apps/parsley/src/hooks/useQueryParam/index.ts | 5 +- apps/parsley/src/utils/query-string/index.ts | 25 ---- .../utils/query-string/query-string.test.ts | 107 +----------------- apps/spruce/src/analytics/addPageAction.ts | 78 ------------- .../aprilFools/useAprilFoolsAnalytics.ts | 5 +- .../breadcrumb/useBreadcrumbAnalytics.ts | 5 +- .../useDistroSettingsAnalytics.ts | 7 +- .../hostsTable/useHostsTableAnalytics.ts | 7 +- .../analytics/joblogs/useJobLogsAnalytics.ts | 5 +- .../analytics/navbar/useNavbarAnalytics.ts | 6 +- .../src/analytics/patch/usePatchAnalytics.ts | 9 +- .../patches/useProjectPatchesAnalytics.ts | 7 +- .../patches/useUserPatchesAnalytics.ts | 5 +- .../preferences/usePreferencesAnalytics.ts | 5 +- .../useProjectHealthAnalytics.ts | 7 +- .../useProjectSettingsAnalytics.ts | 7 +- .../shortcuts/useShortcutAnalytics.ts | 6 +- .../src/analytics/spawn/useSpawnAnalytics.ts | 11 +- .../analytics/task/useAnnotationAnalytics.ts | 28 ++--- .../src/analytics/task/useTaskAnalytics.ts | 17 +-- .../taskQueue/useTaskQueueAnalytics.ts | 5 +- apps/spruce/src/analytics/types.ts | 23 ++++ apps/spruce/src/analytics/useAnalyticsRoot.ts | 30 ----- .../analytics/version/useVersionAnalytics.ts | 9 +- apps/spruce/src/constants/routes.ts | 2 +- apps/spruce/src/gql/generated/types.ts | 69 ----------- .../gql/queries/annotation-event-data.graphql | 11 -- apps/spruce/src/gql/queries/index.ts | 2 - apps/spruce/src/hooks/useQueryParam/index.ts | 6 +- .../src/hooks/useUpdateURLQueryParams.ts | 3 +- .../pages/spawn/spawnHost/SpawnHostCard.tsx | 22 ++-- .../AddIssueModal/index.tsx | 2 +- .../AnnotationNote.tsx | 1 - .../BuildBaron.test.tsx | 26 ----- .../Issues/AnnotationTickets.tsx | 7 -- apps/spruce/src/utils/queryString/index.ts | 1 - .../src/utils/queryString/parseQueryString.ts | 7 -- .../utils/queryString/stringifyQuery.test.ts | 56 --------- .../src/utils/queryString/stringifyQuery.ts | 17 --- .../src/utils/url/updateUrlQueryParam.ts | 3 +- packages/lib/README.md | 49 ++++++++ packages/lib/package.json | 3 +- packages/lib/src/analytics/hooks.ts | 40 +++++++ packages/lib/src/analytics/types.ts | 52 +++++++++ packages/lib/src/analytics/utils.test.ts | 72 ++++++++++++ packages/lib/src/analytics/utils.ts | 29 +++++ packages/lib/src/utils/query-string/index.ts | 37 ++++++ .../utils/query-string/query-string.test.ts | 101 +++++++++++++++++ packages/lib/src/vite-env.d.ts | 10 ++ packages/lib/tsconfig.json | 5 +- 61 files changed, 559 insertions(+), 622 deletions(-) delete mode 100644 apps/parsley/src/analytics/addPageAction.ts create mode 100644 apps/parsley/src/analytics/types.ts delete mode 100644 apps/parsley/src/analytics/useAnalyticsRoot.ts delete mode 100644 apps/spruce/src/analytics/addPageAction.ts create mode 100644 apps/spruce/src/analytics/types.ts delete mode 100644 apps/spruce/src/analytics/useAnalyticsRoot.ts delete mode 100644 apps/spruce/src/gql/queries/annotation-event-data.graphql delete mode 100644 apps/spruce/src/utils/queryString/stringifyQuery.test.ts delete mode 100644 apps/spruce/src/utils/queryString/stringifyQuery.ts create mode 100644 packages/lib/README.md create mode 100644 packages/lib/src/analytics/hooks.ts create mode 100644 packages/lib/src/analytics/types.ts create mode 100644 packages/lib/src/analytics/utils.test.ts create mode 100644 packages/lib/src/analytics/utils.ts create mode 100644 packages/lib/src/utils/query-string/index.ts create mode 100644 packages/lib/src/utils/query-string/query-string.test.ts create mode 100644 packages/lib/src/vite-env.d.ts diff --git a/README.md b/README.md index 10713fcf1..db5126727 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,11 @@ The new home of [Spruce](/apps/spruce) and [Parsley](/apps/parsley). ## Monorepo Tips & Tricks -Check out the [Yarn Workspaces documentation](https://classic.yarnpkg.com/lang/en/docs/workspaces/) for more. +Learn about our monorepo shared library [here](packages/lib/README.md). + +Check out the +[Yarn Workspaces documentation](https://classic.yarnpkg.com/lang/en/docs/workspaces/) +for more. ### Upgrades @@ -27,18 +31,22 @@ For example, `yarn workspace spruce run storybook`. ### Testing To run all unit tests across the repository, from root: + ```bash yarn test ``` To run a particular workspace's unit tests from root: + ```bash yarn test --project [workspace-name] ``` ### Storybook -Spruce, Parsley, and @evg-ui/lib all have their own storybooks, but there's also a shared storybook that combines them into one interface. From root, just run: +Spruce, Parsley, and @evg-ui/lib all have their own storybooks, but there's also +a shared storybook that combines them into one interface. From root, just run: + ```bash yarn storybook ``` diff --git a/apps/parsley/src/analytics/addPageAction.ts b/apps/parsley/src/analytics/addPageAction.ts deleted file mode 100644 index bf4c37526..000000000 --- a/apps/parsley/src/analytics/addPageAction.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { parseQueryString } from "utils/query-string"; - -export interface Analytics { - sendEvent: (action: Action) => void; -} - -export type AnalyticsObject = - | "LogDrop" - | "LogWindow" - | "Preferences" - | "LoadingPage" - | "Shortcut"; - -interface RequiredProperties { - object: AnalyticsObject; -} -type ActionTypePrefixes = - | "Changed" - | "Clicked" - | "Created" - | "Deleted" - | "Redirected" - | "Filtered" - | "Saved" - | "Sorted" - | "Toggled" - | "Viewed" - | "Used" - | "System Event"; - -export interface ActionType { - name: `${ActionTypePrefixes}${string}`; -} - -export interface Properties { - [key: string]: string | number; -} - -export const addPageAction = ( - { name, ...actionProps }: A, - properties: P & RequiredProperties, -) => { - const { search } = window.location; - const attributesToSend = { - ...properties, - ...parseQueryString(search), - ...actionProps, - }; - - if (typeof window?.newrelic !== "object") { - // These will only print when new relic is not available such as during local development - console.log("ANALYTICS EVENT ", { attributesToSend, name }); - return; - } - - window.newrelic.addPageAction(name, attributesToSend); -}; diff --git a/apps/parsley/src/analytics/loadingPage/useLogDownloadAnalytics.ts b/apps/parsley/src/analytics/loadingPage/useLogDownloadAnalytics.ts index 53abc1972..8c2c4b488 100644 --- a/apps/parsley/src/analytics/loadingPage/useLogDownloadAnalytics.ts +++ b/apps/parsley/src/analytics/loadingPage/useLogDownloadAnalytics.ts @@ -1,4 +1,5 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { LogTypes } from "constants/enums"; type Action = @@ -22,4 +23,4 @@ type Action = }; export const useLogDownloadAnalytics = () => - useAnalyticsRoot("LoadingPage"); + useAnalyticsRoot("LoadingPage"); diff --git a/apps/parsley/src/analytics/logDrop/useLogDropAnalytics.ts b/apps/parsley/src/analytics/logDrop/useLogDropAnalytics.ts index c5daa3d51..85059b621 100644 --- a/apps/parsley/src/analytics/logDrop/useLogDropAnalytics.ts +++ b/apps/parsley/src/analytics/logDrop/useLogDropAnalytics.ts @@ -1,4 +1,5 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { LogTypes } from "constants/enums"; type Action = @@ -10,4 +11,5 @@ type Action = fileSize?: number; }; -export const useLogDropAnalytics = () => useAnalyticsRoot("LogDrop"); +export const useLogDropAnalytics = () => + useAnalyticsRoot("LogDrop"); diff --git a/apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts b/apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts index 1937bdfb8..dc9d07340 100644 --- a/apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts +++ b/apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts @@ -1,4 +1,5 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { DIRECTION } from "context/LogContext/types"; import { Filter } from "types/logs"; @@ -38,4 +39,4 @@ type Action = }; export const useLogWindowAnalytics = () => - useAnalyticsRoot("LogWindow"); + useAnalyticsRoot("LogWindow"); diff --git a/apps/parsley/src/analytics/preferences/usePreferencesAnalytics.ts b/apps/parsley/src/analytics/preferences/usePreferencesAnalytics.ts index d82a75927..55b54bf06 100644 --- a/apps/parsley/src/analytics/preferences/usePreferencesAnalytics.ts +++ b/apps/parsley/src/analytics/preferences/usePreferencesAnalytics.ts @@ -1,4 +1,5 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { FilterLogic, WordWrapFormat } from "constants/enums"; type Action = @@ -19,4 +20,4 @@ type Action = | { name: "Toggled sections"; on: boolean }; export const usePreferencesAnalytics = () => - useAnalyticsRoot("Preferences"); + useAnalyticsRoot("Preferences"); diff --git a/apps/parsley/src/analytics/shortcuts/useShortcutAnalytics.ts b/apps/parsley/src/analytics/shortcuts/useShortcutAnalytics.ts index 9e78384fa..7b8a18ff4 100644 --- a/apps/parsley/src/analytics/shortcuts/useShortcutAnalytics.ts +++ b/apps/parsley/src/analytics/shortcuts/useShortcutAnalytics.ts @@ -1,5 +1,7 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; type Action = { name: "Used shortcut"; keys: string }; -export const useShortcutAnalytics = () => useAnalyticsRoot("Shortcut"); +export const useShortcutAnalytics = () => + useAnalyticsRoot("Shortcut"); diff --git a/apps/parsley/src/analytics/types.ts b/apps/parsley/src/analytics/types.ts new file mode 100644 index 000000000..4193d4a7a --- /dev/null +++ b/apps/parsley/src/analytics/types.ts @@ -0,0 +1,6 @@ +export type AnalyticsIdentifier = + | "LogDrop" + | "LogWindow" + | "Preferences" + | "LoadingPage" + | "Shortcut"; diff --git a/apps/parsley/src/analytics/useAnalyticsRoot.ts b/apps/parsley/src/analytics/useAnalyticsRoot.ts deleted file mode 100644 index c489a954e..000000000 --- a/apps/parsley/src/analytics/useAnalyticsRoot.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useCallback, useMemo } from "react"; -import { - ActionType, - Analytics, - AnalyticsObject, - Properties, - addPageAction, -} from "analytics/addPageAction"; - -interface P extends Properties {} - -export const useAnalyticsRoot = ( - object: AnalyticsObject, -): Analytics => { - const sendEvent: Analytics["sendEvent"] = useCallback( - (action) => { - addPageAction(action, { object }); - }, - [object], - ); - - return useMemo(() => ({ sendEvent }), [sendEvent]); -}; diff --git a/apps/parsley/src/constants/externalURLTemplates.ts b/apps/parsley/src/constants/externalURLTemplates.ts index ba6413fc4..74b525885 100644 --- a/apps/parsley/src/constants/externalURLTemplates.ts +++ b/apps/parsley/src/constants/externalURLTemplates.ts @@ -1,5 +1,5 @@ +import { stringifyQuery } from "@evg-ui/lib/utils/query-string"; import { evergreenURL, spruceURL } from "utils/environmentVariables"; -import { stringifyQuery } from "utils/query-string"; const getEvergreenTaskURL = (taskID: string, execution: string | number) => { const params = { diff --git a/apps/parsley/src/constants/logURLTemplates.ts b/apps/parsley/src/constants/logURLTemplates.ts index 718325ebd..17561ea83 100644 --- a/apps/parsley/src/constants/logURLTemplates.ts +++ b/apps/parsley/src/constants/logURLTemplates.ts @@ -1,7 +1,7 @@ import queryString from "query-string"; +import { stringifyQuery } from "@evg-ui/lib/utils/query-string"; import { Task as TaskType } from "gql/generated/types"; import { evergreenURL, logkeeperURL } from "utils/environmentVariables"; -import { stringifyQuery } from "utils/query-string"; /** * diff --git a/apps/parsley/src/hooks/useQueryParam/index.ts b/apps/parsley/src/hooks/useQueryParam/index.ts index 93c6fbe22..535e11895 100644 --- a/apps/parsley/src/hooks/useQueryParam/index.ts +++ b/apps/parsley/src/hooks/useQueryParam/index.ts @@ -2,8 +2,11 @@ import { useCallback, useMemo } from "react"; import { ParseOptions } from "query-string"; import { useNavigate, useSearchParams } from "react-router-dom"; import { conditionalToArray } from "@evg-ui/lib/utils/array"; +import { + parseQueryString, + stringifyQuery, +} from "@evg-ui/lib/utils/query-string"; import { QueryParams } from "constants/queryParams"; -import { parseQueryString, stringifyQuery } from "utils/query-string"; /** * `useQueryParams` returns all of the query params that exist in the url. diff --git a/apps/parsley/src/utils/query-string/index.ts b/apps/parsley/src/utils/query-string/index.ts index cd13daeed..4316bb30c 100644 --- a/apps/parsley/src/utils/query-string/index.ts +++ b/apps/parsley/src/utils/query-string/index.ts @@ -1,31 +1,6 @@ -import queryString, { ParseOptions, StringifyOptions } from "query-string"; import { CaseSensitivity, MatchType } from "constants/enums"; import { Filters } from "types/logs"; -export const parseQueryString = ( - search: string, - options: ParseOptions = {}, -) => { - const parseOptions: ParseOptions = { - arrayFormat: "comma", - parseBooleans: true, - parseNumbers: true, - ...options, - }; - return queryString.parse(search, parseOptions); -}; - -export const stringifyQuery = ( - object: { [key: string]: any }, - options: StringifyOptions = {}, -) => - queryString.stringify(object, { - arrayFormat: "comma", - skipEmptyString: true, - skipNull: true, - ...options, - }); - export const parseFilters = (filters: string[]): Filters => { const parsedFilters: Filters = filters.map((f) => { // Ensure that a filter is a string before parsing it. diff --git a/apps/parsley/src/utils/query-string/query-string.test.ts b/apps/parsley/src/utils/query-string/query-string.test.ts index 8841ce2fe..0b2a3b43b 100644 --- a/apps/parsley/src/utils/query-string/query-string.test.ts +++ b/apps/parsley/src/utils/query-string/query-string.test.ts @@ -1,10 +1,5 @@ import { CaseSensitivity, MatchType } from "constants/enums"; -import { - parseFilters, - parseQueryString, - stringifyFilters, - stringifyQuery, -} from "."; +import { parseFilters, stringifyFilters } from "."; describe("filters", () => { describe("stringifyFilters", () => { @@ -120,103 +115,3 @@ describe("filters", () => { }); }); }); - -describe("query-string", () => { - describe("stringifyQuery", () => { - it("ignores null", () => { - expect(stringifyQuery({ a: "hello", b: null })).toBe("a=hello"); - }); - it("ignores emptyStrings", () => { - expect(stringifyQuery({ a: "hello", b: "" })).toBe("a=hello"); - }); - it("should preserve empty strings if skipEmptyString is passed in", () => { - let result = stringifyQuery( - { bar: null, foo: "" }, - { skipEmptyString: false }, - ); - expect(result).toBe("foo="); - result = stringifyQuery({ bar: 21, foo: "" }, { skipEmptyString: false }); - expect(result).toBe("bar=21&foo="); - }); - it("can handle empty input", () => { - expect(stringifyQuery({})).toBe(""); - }); - it("stringifies a boolean correctly", () => { - expect(stringifyQuery({ exists: true })).toBe("exists=true"); - }); - it("stringifies a number correctly", () => { - expect(stringifyQuery({ files: 23 })).toBe("files=23"); - }); - it("stringifies an array correctly", () => { - expect( - stringifyQuery({ statuses: ["passed", "failed", "running"] }), - ).toBe("statuses=passed,failed,running"); - }); - it("stringifies an object containing many fields correctly", () => { - expect( - stringifyQuery({ - exists: true, - files: 23, - statuses: ["passed", "failed", "running"], - variant: [1, 3, 5], - }), - ).toBe( - "exists=true&files=23&statuses=passed,failed,running&variant=1,3,5", - ); - }); - }); - - describe("parseQueryString", () => { - it("parses a single query param with a string", () => { - expect(parseQueryString("status=passed")).toMatchObject({ - status: "passed", - }); - }); - it("parses multiple query params that are strings", () => { - expect( - parseQueryString("status=passed&variant=ubuntu1604"), - ).toMatchObject({ - status: "passed", - variant: "ubuntu1604", - }); - }); - it("parses a query param with an array as a value", () => { - expect(parseQueryString("statuses=failed,passed,ehh")).toMatchObject({ - statuses: ["failed", "passed", "ehh"], - }); - }); - it("parses a query param with multiple arrays as value", () => { - expect( - parseQueryString( - "statuses=failed,passed,ehh&variants=ubuntu1604,GLADOS", - ), - ).toMatchObject({ - statuses: ["failed", "passed", "ehh"], - variants: ["ubuntu1604", "GLADOS"], - }); - }); - it("parses a query param with a mixed array and a single string as a value", () => { - expect( - parseQueryString("status=failed&variants=ubuntu1604,GLADOS"), - ).toMatchObject({ - status: "failed", - variants: ["ubuntu1604", "GLADOS"], - }); - }); - it("parses a query param with a boolean as a value", () => { - expect(parseQueryString("status=true")).toMatchObject({ - status: true, - }); - }); - it("parses a query param with a number as a value", () => { - expect(parseQueryString("status=1")).toMatchObject({ - status: 1, - }); - }); - it("parses a query param with a number array as a value", () => { - expect(parseQueryString("status=1,2,3")).toMatchObject({ - status: [1, 2, 3], - }); - }); - }); -}); diff --git a/apps/spruce/src/analytics/addPageAction.ts b/apps/spruce/src/analytics/addPageAction.ts deleted file mode 100644 index 50d13bcfe..000000000 --- a/apps/spruce/src/analytics/addPageAction.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { queryString } from "utils"; - -const { parseQueryString } = queryString; - -export interface Analytics { - sendEvent: (action: Action) => void; -} - -export type AnalyticsObject = - | "AllHostsPage" - | "Annotations" - | "April Fools" - | "Breadcrumb" - | "CommitQueue" - | "Configure" - | "DistroSettings" - | "HostPage" - | "JobLogs" - | "Navbar" - | "Patch" - | "Polling" - | "PreferencesPages" - | "ProjectHealthPages" - | "ProjectPatches" - | "ProjectSettings" - | "Shortcut" - | "SpawnPages" - | "Task" - | "TaskQueue" - | "UserPatches" - | "Version"; - -interface RequiredProperties { - object: AnalyticsObject; -} - -type ActionTypePrefixes = - | "Changed" - | "Clicked" - | "Created" - | "Deleted" - | "Redirected" - | "Filtered" - | "Saved" - | "Sorted" - | "Toggled" - | "Viewed" - | "Used" - | "System Event"; - -export interface ActionType { - name: `${ActionTypePrefixes}${string}`; -} - -export interface Properties { - [key: string]: string | number; -} - -export const addPageAction = ( - { name, ...actionProps }: A, - properties: P & RequiredProperties, -) => { - const { newrelic } = window; - const { search } = window.location; - const attributesToSend = { - ...properties, - ...parseQueryString(search), - ...actionProps, - }; - - if (typeof newrelic !== "object") { - // These will only print when new relic is not available such as during local development - console.log("ANALYTICS EVENT ", { name, attributesToSend }); - return; - } - - newrelic.addPageAction(name, attributesToSend); -}; diff --git a/apps/spruce/src/analytics/aprilFools/useAprilFoolsAnalytics.ts b/apps/spruce/src/analytics/aprilFools/useAprilFoolsAnalytics.ts index 75b1d84ac..e35b6035e 100644 --- a/apps/spruce/src/analytics/aprilFools/useAprilFoolsAnalytics.ts +++ b/apps/spruce/src/analytics/aprilFools/useAprilFoolsAnalytics.ts @@ -1,6 +1,7 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; type Action = { name: "Used 2024 Boilerplate!" }; export const useAprilFoolsAnalytics = () => - useAnalyticsRoot("April Fools"); + useAnalyticsRoot("April Fools"); diff --git a/apps/spruce/src/analytics/breadcrumb/useBreadcrumbAnalytics.ts b/apps/spruce/src/analytics/breadcrumb/useBreadcrumbAnalytics.ts index 16772653c..5ddb9c92d 100644 --- a/apps/spruce/src/analytics/breadcrumb/useBreadcrumbAnalytics.ts +++ b/apps/spruce/src/analytics/breadcrumb/useBreadcrumbAnalytics.ts @@ -1,4 +1,5 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; type Action = { name: "Clicked link"; @@ -6,4 +7,4 @@ type Action = { }; export const useBreadcrumbAnalytics = () => - useAnalyticsRoot("Breadcrumb"); + useAnalyticsRoot("Breadcrumb"); diff --git a/apps/spruce/src/analytics/distroSettings/useDistroSettingsAnalytics.ts b/apps/spruce/src/analytics/distroSettings/useDistroSettingsAnalytics.ts index 1f6d2a43e..683b6ea7e 100644 --- a/apps/spruce/src/analytics/distroSettings/useDistroSettingsAnalytics.ts +++ b/apps/spruce/src/analytics/distroSettings/useDistroSettingsAnalytics.ts @@ -1,5 +1,6 @@ import { useParams } from "react-router-dom"; -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { slugs } from "constants/routes"; type Action = @@ -9,5 +10,7 @@ type Action = export const useDistroSettingsAnalytics = () => { const { [slugs.distroId]: distroId } = useParams(); - return useAnalyticsRoot("DistroSettings", { distroId }); + return useAnalyticsRoot("DistroSettings", { + "distro.id": distroId || "", + }); }; diff --git a/apps/spruce/src/analytics/hostsTable/useHostsTableAnalytics.ts b/apps/spruce/src/analytics/hostsTable/useHostsTableAnalytics.ts index 4e15eeed6..3d132146a 100644 --- a/apps/spruce/src/analytics/hostsTable/useHostsTableAnalytics.ts +++ b/apps/spruce/src/analytics/hostsTable/useHostsTableAnalytics.ts @@ -1,4 +1,5 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; type Action = | { name: "Filtered hosts table"; filterBy: string | string[] } @@ -9,4 +10,6 @@ type Action = | { name: "Clicked update host status button"; status: string }; export const useHostsTableAnalytics = (isHostPage?: boolean) => - useAnalyticsRoot(isHostPage ? "HostPage" : "AllHostsPage"); + useAnalyticsRoot( + isHostPage ? "HostPage" : "AllHostsPage", + ); diff --git a/apps/spruce/src/analytics/joblogs/useJobLogsAnalytics.ts b/apps/spruce/src/analytics/joblogs/useJobLogsAnalytics.ts index a674e8ead..f1b50b6aa 100644 --- a/apps/spruce/src/analytics/joblogs/useJobLogsAnalytics.ts +++ b/apps/spruce/src/analytics/joblogs/useJobLogsAnalytics.ts @@ -1,4 +1,5 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; type Action = | { @@ -11,6 +12,6 @@ type Action = | { name: "Clicked Parsley test log link"; buildId?: string }; export const useJobLogsAnalytics = (isLogkeeper: boolean) => - useAnalyticsRoot("JobLogs", { + useAnalyticsRoot("JobLogs", { isLogkeeperHostedLog: isLogkeeper, }); diff --git a/apps/spruce/src/analytics/navbar/useNavbarAnalytics.ts b/apps/spruce/src/analytics/navbar/useNavbarAnalytics.ts index 5524b9b5f..dc1002f94 100644 --- a/apps/spruce/src/analytics/navbar/useNavbarAnalytics.ts +++ b/apps/spruce/src/analytics/navbar/useNavbarAnalytics.ts @@ -1,4 +1,5 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; type Action = | { name: "Clicked admin settings link" } @@ -17,4 +18,5 @@ type Action = | { name: "Clicked task queue link" } | { name: "Clicked commit queue link" }; -export const useNavbarAnalytics = () => useAnalyticsRoot("Navbar"); +export const useNavbarAnalytics = () => + useAnalyticsRoot("Navbar"); diff --git a/apps/spruce/src/analytics/patch/usePatchAnalytics.ts b/apps/spruce/src/analytics/patch/usePatchAnalytics.ts index 11569c0fd..a70001a97 100644 --- a/apps/spruce/src/analytics/patch/usePatchAnalytics.ts +++ b/apps/spruce/src/analytics/patch/usePatchAnalytics.ts @@ -1,5 +1,6 @@ import { useQuery } from "@apollo/client"; -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { PatchQuery, PatchQueryVariables, @@ -27,8 +28,8 @@ export const usePatchAnalytics = (id: string) => { }); const { status } = eventData?.patch || {}; - return useAnalyticsRoot("Patch", { - patchStatus: status, - patchId: id, + return useAnalyticsRoot("Patch", { + "patch.status": status || "", + "patch.id": id, }); }; diff --git a/apps/spruce/src/analytics/patches/useProjectPatchesAnalytics.ts b/apps/spruce/src/analytics/patches/useProjectPatchesAnalytics.ts index dd6240909..c1757141a 100644 --- a/apps/spruce/src/analytics/patches/useProjectPatchesAnalytics.ts +++ b/apps/spruce/src/analytics/patches/useProjectPatchesAnalytics.ts @@ -1,5 +1,6 @@ import { useParams } from "react-router-dom"; -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { slugs } from "constants/routes"; type Action = @@ -15,5 +16,7 @@ type Action = export const useProjectPatchesAnalytics = () => { const { [slugs.projectIdentifier]: projectIdentifier } = useParams(); - return useAnalyticsRoot("ProjectPatches", { projectIdentifier }); + return useAnalyticsRoot("ProjectPatches", { + "project.identifier": projectIdentifier || "", + }); }; diff --git a/apps/spruce/src/analytics/patches/useUserPatchesAnalytics.ts b/apps/spruce/src/analytics/patches/useUserPatchesAnalytics.ts index 4e2aa355a..484f5329d 100644 --- a/apps/spruce/src/analytics/patches/useUserPatchesAnalytics.ts +++ b/apps/spruce/src/analytics/patches/useUserPatchesAnalytics.ts @@ -1,4 +1,5 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; type Action = | { name: "Changed page size" } @@ -11,4 +12,4 @@ type Action = }; export const useUserPatchesAnalytics = () => - useAnalyticsRoot("UserPatches"); + useAnalyticsRoot("UserPatches"); diff --git a/apps/spruce/src/analytics/preferences/usePreferencesAnalytics.ts b/apps/spruce/src/analytics/preferences/usePreferencesAnalytics.ts index 5c6026a18..ee5dc80ec 100644 --- a/apps/spruce/src/analytics/preferences/usePreferencesAnalytics.ts +++ b/apps/spruce/src/analytics/preferences/usePreferencesAnalytics.ts @@ -1,4 +1,5 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { UpdateUserSettingsMutationVariables } from "gql/generated/types"; type Action = @@ -16,4 +17,4 @@ type Action = | { name: "Toggled polling"; value: "Enabled" | "Disabled" }; export const usePreferencesAnalytics = () => - useAnalyticsRoot("PreferencesPages"); + useAnalyticsRoot("PreferencesPages"); diff --git a/apps/spruce/src/analytics/projectHealth/useProjectHealthAnalytics.ts b/apps/spruce/src/analytics/projectHealth/useProjectHealthAnalytics.ts index 220f36ac6..a405510e7 100644 --- a/apps/spruce/src/analytics/projectHealth/useProjectHealthAnalytics.ts +++ b/apps/spruce/src/analytics/projectHealth/useProjectHealthAnalytics.ts @@ -1,4 +1,5 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { ProjectHealthView, SaveSubscriptionForUserMutationVariables, @@ -49,4 +50,6 @@ type Action = | { name: "Viewed task history page" }; // "Commit chart" export const useProjectHealthAnalytics = (p: { page: pageType }) => - useAnalyticsRoot("ProjectHealthPages", { page: p.page }); + useAnalyticsRoot("ProjectHealthPages", { + page: p.page, + }); diff --git a/apps/spruce/src/analytics/projectSettings/useProjectSettingsAnalytics.ts b/apps/spruce/src/analytics/projectSettings/useProjectSettingsAnalytics.ts index f3ca7348d..ec8aad7e6 100644 --- a/apps/spruce/src/analytics/projectSettings/useProjectSettingsAnalytics.ts +++ b/apps/spruce/src/analytics/projectSettings/useProjectSettingsAnalytics.ts @@ -1,5 +1,6 @@ import { useParams } from "react-router-dom"; -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { slugs } from "constants/routes"; type Action = @@ -31,5 +32,7 @@ type Action = export const useProjectSettingsAnalytics = () => { const { [slugs.projectIdentifier]: projectIdentifier } = useParams(); - return useAnalyticsRoot("ProjectSettings", { projectIdentifier }); + return useAnalyticsRoot("ProjectSettings", { + "project.identifier": projectIdentifier || "", + }); }; diff --git a/apps/spruce/src/analytics/shortcuts/useShortcutAnalytics.ts b/apps/spruce/src/analytics/shortcuts/useShortcutAnalytics.ts index 33745da70..cfe01a9e4 100644 --- a/apps/spruce/src/analytics/shortcuts/useShortcutAnalytics.ts +++ b/apps/spruce/src/analytics/shortcuts/useShortcutAnalytics.ts @@ -1,5 +1,7 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; type Action = { name: "Used Shortcut"; keys: string }; -export const useShortcutAnalytics = () => useAnalyticsRoot("Shortcut"); +export const useShortcutAnalytics = () => + useAnalyticsRoot("Shortcut"); diff --git a/apps/spruce/src/analytics/spawn/useSpawnAnalytics.ts b/apps/spruce/src/analytics/spawn/useSpawnAnalytics.ts index 3b115e51b..ed6b1efe9 100644 --- a/apps/spruce/src/analytics/spawn/useSpawnAnalytics.ts +++ b/apps/spruce/src/analytics/spawn/useSpawnAnalytics.ts @@ -1,5 +1,5 @@ -import { Analytics } from "analytics/addPageAction"; -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { EditSpawnHostMutationVariables, SpawnHostMutationVariables, @@ -40,8 +40,5 @@ type Action = | { name: "Clicked open IDE button" } | { name: "Changed tab"; tab: string }; -export const useSpawnAnalytics = () => useAnalyticsRoot("SpawnPages"); - -type SpawnHostAnalytics = Analytics; - -export type { SpawnHostAnalytics as Analytics }; +export const useSpawnAnalytics = () => + useAnalyticsRoot("SpawnPages"); diff --git a/apps/spruce/src/analytics/task/useAnnotationAnalytics.ts b/apps/spruce/src/analytics/task/useAnnotationAnalytics.ts index 7f25225bb..94eaf5af8 100644 --- a/apps/spruce/src/analytics/task/useAnnotationAnalytics.ts +++ b/apps/spruce/src/analytics/task/useAnnotationAnalytics.ts @@ -1,14 +1,10 @@ import { useQuery } from "@apollo/client"; import { useParams } from "react-router-dom"; -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { slugs } from "constants/routes"; -import { - BuildBaronQuery, - BuildBaronQueryVariables, - AnnotationEventDataQuery, - AnnotationEventDataQueryVariables, -} from "gql/generated/types"; -import { ANNOTATION_EVENT_DATA, BUILD_BARON } from "gql/queries"; +import { BuildBaronQuery, BuildBaronQueryVariables } from "gql/generated/types"; +import { BUILD_BARON } from "gql/queries"; import { useQueryParam } from "hooks/useQueryParam"; import { RequiredQueryParams } from "types/task"; @@ -31,14 +27,6 @@ export const useAnnotationAnalytics = () => { const { [slugs.taskId]: taskId } = useParams(); const [execution] = useQueryParam(RequiredQueryParams.Execution, 0); - const { data: eventData } = useQuery< - AnnotationEventDataQuery, - AnnotationEventDataQueryVariables - >(ANNOTATION_EVENT_DATA, { - variables: { taskId: taskId || "", execution }, - fetchPolicy: "cache-first", - }); - const { data: bbData } = useQuery( BUILD_BARON, { @@ -47,12 +35,10 @@ export const useAnnotationAnalytics = () => { }, ); - const { annotation } = eventData?.task || {}; const { buildBaronConfigured } = bbData?.buildBaron || {}; - return useAnalyticsRoot("Annotations", { - taskId, - annotation, - bbConfigured: buildBaronConfigured, + return useAnalyticsRoot("Annotations", { + "task.id": taskId || "", + buildBaronConfigured: buildBaronConfigured || false, }); }; diff --git a/apps/spruce/src/analytics/task/useTaskAnalytics.ts b/apps/spruce/src/analytics/task/useTaskAnalytics.ts index 52c9e94b6..303d0f4ba 100644 --- a/apps/spruce/src/analytics/task/useTaskAnalytics.ts +++ b/apps/spruce/src/analytics/task/useTaskAnalytics.ts @@ -1,6 +1,7 @@ import { useQuery } from "@apollo/client"; import { useParams } from "react-router-dom"; -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { slugs } from "constants/routes"; import { SaveSubscriptionForUserMutationVariables, @@ -87,12 +88,12 @@ export const useTaskAnalytics = () => { } = eventData?.task || {}; const isLatestExecution = latestExecution === execution; - return useAnalyticsRoot("Task", { - taskStatus, - execution, - isLatestExecution: isLatestExecution.toString(), - taskId, - failedTestCount, - projectIdentifier: identifier, + return useAnalyticsRoot("Task", { + "task.status": taskStatus || "", + "task.execution": execution, + "task.isLatestExecution": isLatestExecution, + "task.id": taskId || "", + "task.failedTestCount": failedTestCount || "", + "task.project.identifier": identifier, }); }; diff --git a/apps/spruce/src/analytics/taskQueue/useTaskQueueAnalytics.ts b/apps/spruce/src/analytics/taskQueue/useTaskQueueAnalytics.ts index dc469a83f..69d7ca5c5 100644 --- a/apps/spruce/src/analytics/taskQueue/useTaskQueueAnalytics.ts +++ b/apps/spruce/src/analytics/taskQueue/useTaskQueueAnalytics.ts @@ -1,4 +1,5 @@ -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; type Action = | { name: "Changed distro"; distro: string } @@ -8,4 +9,4 @@ type Action = | { name: "Clicked activated by link" }; export const useTaskQueueAnalytics = () => - useAnalyticsRoot("TaskQueue"); + useAnalyticsRoot("TaskQueue"); diff --git a/apps/spruce/src/analytics/types.ts b/apps/spruce/src/analytics/types.ts new file mode 100644 index 000000000..59b593190 --- /dev/null +++ b/apps/spruce/src/analytics/types.ts @@ -0,0 +1,23 @@ +export type AnalyticsIdentifier = + | "AllHostsPage" + | "Annotations" + | "April Fools" + | "Breadcrumb" + | "CommitQueue" + | "Configure" + | "DistroSettings" + | "HostPage" + | "JobLogs" + | "Navbar" + | "Patch" + | "Polling" + | "PreferencesPages" + | "ProjectHealthPages" + | "ProjectPatches" + | "ProjectSettings" + | "Shortcut" + | "SpawnPages" + | "Task" + | "TaskQueue" + | "UserPatches" + | "Version"; diff --git a/apps/spruce/src/analytics/useAnalyticsRoot.ts b/apps/spruce/src/analytics/useAnalyticsRoot.ts deleted file mode 100644 index d4670fb9a..000000000 --- a/apps/spruce/src/analytics/useAnalyticsRoot.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useCallback, useMemo } from "react"; -import { - ActionType, - Analytics, - AnalyticsObject, - Properties, - addPageAction, -} from "analytics/addPageAction"; - -interface P extends Properties {} - -export const useAnalyticsRoot = ( - object: AnalyticsObject, - attributes: { [key: string]: any } = {}, -): Analytics => { - const sendEvent: Analytics["sendEvent"] = useCallback( - (action) => { - const userId = localStorage.getItem("userId"); - addPageAction(action, { - object, - // @ts-expect-error: FIXME. This comment was added by an automated script. - userId, - ...attributes, - }); - }, - [object, attributes], - ); - - return useMemo(() => ({ sendEvent }), [sendEvent]); -}; diff --git a/apps/spruce/src/analytics/version/useVersionAnalytics.ts b/apps/spruce/src/analytics/version/useVersionAnalytics.ts index b484be2b7..e1fba8b91 100644 --- a/apps/spruce/src/analytics/version/useVersionAnalytics.ts +++ b/apps/spruce/src/analytics/version/useVersionAnalytics.ts @@ -1,5 +1,6 @@ import { useQuery } from "@apollo/client"; -import { useAnalyticsRoot } from "analytics/useAnalyticsRoot"; +import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; +import { AnalyticsIdentifier } from "analytics/types"; import { SaveSubscriptionForUserMutationVariables, VersionQuery, @@ -62,8 +63,8 @@ export const useVersionAnalytics = (id: string) => { ); const { status } = eventData?.version || {}; - return useAnalyticsRoot("Version", { - versionStatus: status, - versionId: id, + return useAnalyticsRoot("Version", { + "version.status": status || "", + "version.id": id, }); }; diff --git a/apps/spruce/src/constants/routes.ts b/apps/spruce/src/constants/routes.ts index 23d508250..d6c38696a 100644 --- a/apps/spruce/src/constants/routes.ts +++ b/apps/spruce/src/constants/routes.ts @@ -1,10 +1,10 @@ +import { stringifyQuery } from "@evg-ui/lib/src/utils/query-string"; import { getGithubCommitUrl } from "constants/externalResources"; import { TestStatus, HistoryQueryParams } from "types/history"; import { PatchTab } from "types/patch"; import { PatchTasksQueryParams, TaskTab } from "types/task"; import { ProjectTriggerLevel } from "types/triggers"; import { toArray } from "utils/array"; -import { stringifyQuery } from "utils/queryString"; export enum PageNames { Patches = "patches", diff --git a/apps/spruce/src/gql/generated/types.ts b/apps/spruce/src/gql/generated/types.ts index 7be62460b..7335bae05 100644 --- a/apps/spruce/src/gql/generated/types.ts +++ b/apps/spruce/src/gql/generated/types.ts @@ -5589,75 +5589,6 @@ export type AllLogsQuery = { } | null; }; -export type AnnotationEventDataQueryVariables = Exact<{ - taskId: Scalars["String"]["input"]; - execution?: InputMaybe; -}>; - -export type AnnotationEventDataQuery = { - __typename?: "Query"; - task?: { - __typename?: "Task"; - execution: number; - id: string; - annotation?: { - __typename?: "Annotation"; - id: string; - taskExecution: number; - taskId: string; - webhookConfigured: boolean; - createdIssues?: Array<{ - __typename?: "IssueLink"; - issueKey?: string | null; - url?: string | null; - source?: { - __typename?: "Source"; - author: string; - requester: string; - time: Date; - } | null; - }> | null; - issues?: Array<{ - __typename?: "IssueLink"; - issueKey?: string | null; - url?: string | null; - source?: { - __typename?: "Source"; - author: string; - requester: string; - time: Date; - } | null; - }> | null; - metadataLinks?: Array<{ - __typename?: "MetadataLink"; - text: string; - url: string; - }> | null; - note?: { - __typename?: "Note"; - message: string; - source: { - __typename?: "Source"; - author: string; - requester: string; - time: Date; - }; - } | null; - suspectedIssues?: Array<{ - __typename?: "IssueLink"; - issueKey?: string | null; - url?: string | null; - source?: { - __typename?: "Source"; - author: string; - requester: string; - time: Date; - } | null; - }> | null; - } | null; - } | null; -}; - export type AwsRegionsQueryVariables = Exact<{ [key: string]: never }>; export type AwsRegionsQuery = { diff --git a/apps/spruce/src/gql/queries/annotation-event-data.graphql b/apps/spruce/src/gql/queries/annotation-event-data.graphql deleted file mode 100644 index cdb97402c..000000000 --- a/apps/spruce/src/gql/queries/annotation-event-data.graphql +++ /dev/null @@ -1,11 +0,0 @@ -#import "../fragments/annotation.graphql" - -query AnnotationEventData($taskId: String!, $execution: Int) { - task(taskId: $taskId, execution: $execution) { - annotation { - ...Annotation - } - execution - id - } -} diff --git a/apps/spruce/src/gql/queries/index.ts b/apps/spruce/src/gql/queries/index.ts index 1e6e451b8..f025a4bb7 100644 --- a/apps/spruce/src/gql/queries/index.ts +++ b/apps/spruce/src/gql/queries/index.ts @@ -1,6 +1,5 @@ import AGENT_LOGS from "./agent-logs.graphql"; import ALL_LOGS from "./all-logs.graphql"; -import ANNOTATION_EVENT_DATA from "./annotation-event-data.graphql"; import AWS_REGIONS from "./aws-regions.graphql"; import BASE_VERSION_AND_TASK from "./base-version-and-task.graphql"; import BUILD_BARON_CONFIGURED from "./build-baron-configured.graphql"; @@ -87,7 +86,6 @@ import VIEWABLE_PROJECTS from "./viewable-projects.graphql"; export { AGENT_LOGS, ALL_LOGS, - ANNOTATION_EVENT_DATA, AWS_REGIONS, BASE_VERSION_AND_TASK, BUILD_BARON_CONFIGURED, diff --git a/apps/spruce/src/hooks/useQueryParam/index.ts b/apps/spruce/src/hooks/useQueryParam/index.ts index 0bd56b2c4..852103cc1 100644 --- a/apps/spruce/src/hooks/useQueryParam/index.ts +++ b/apps/spruce/src/hooks/useQueryParam/index.ts @@ -2,9 +2,9 @@ import { useCallback, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; import { conditionalToArray } from "@evg-ui/lib/utils/array"; import { - parseQueryStringAsValue as parseQueryString, - stringifyQueryAsValue as stringifyQuery, -} from "utils/queryString"; + stringifyQuery, + parseQueryString, +} from "@evg-ui/lib/utils/query-string"; /** * `useQueryParams` returns all of the query params passed into the url and a function to update them. diff --git a/apps/spruce/src/hooks/useUpdateURLQueryParams.ts b/apps/spruce/src/hooks/useUpdateURLQueryParams.ts index d51a95c09..d58f4b97d 100644 --- a/apps/spruce/src/hooks/useUpdateURLQueryParams.ts +++ b/apps/spruce/src/hooks/useUpdateURLQueryParams.ts @@ -1,8 +1,9 @@ import { useCallback } from "react"; import { useNavigate, useLocation } from "react-router-dom"; +import { stringifyQuery } from "@evg-ui/lib/utils/query-string"; import { queryString } from "utils"; -const { parseQueryString, stringifyQuery } = queryString; +const { parseQueryString } = queryString; export const useUpdateURLQueryParams = () => { const navigate = useNavigate(); diff --git a/apps/spruce/src/pages/spawn/spawnHost/SpawnHostCard.tsx b/apps/spruce/src/pages/spawn/spawnHost/SpawnHostCard.tsx index d1d02eec0..e8a4d6be7 100644 --- a/apps/spruce/src/pages/spawn/spawnHost/SpawnHostCard.tsx +++ b/apps/spruce/src/pages/spawn/spawnHost/SpawnHostCard.tsx @@ -1,10 +1,8 @@ import styled from "@emotion/styled"; import Badge from "@leafygreen-ui/badge"; import { InfoSprinkle } from "@leafygreen-ui/info-sprinkle"; -import { - Analytics, - useSpawnAnalytics, -} from "analytics/spawn/useSpawnAnalytics"; +import { ExtractAnalyticsSendEvent } from "@evg-ui/lib/analytics/types"; +import { useSpawnAnalytics } from "analytics/spawn/useSpawnAnalytics"; import { DoesNotExpire, DetailsCard } from "components/Spawn"; import { StyledLink, StyledRouterLink } from "components/styles"; import { getIdeUrl } from "constants/externalResources"; @@ -15,8 +13,6 @@ import { HostStatus } from "types/host"; import { MyHost } from "types/spawn"; import { workstationSupportedDistros } from "./constants"; -type SendEvent = Analytics["sendEvent"]; - interface SpawnHostCardProps { host: MyHost; } @@ -36,16 +32,18 @@ const SpawnHostCard: React.FC = ({ host }) => { const HostUptime: React.FC = ({ uptime }) => { const getDateCopy = useDateFormat(); - // @ts-expect-error: FIXME. This comment was added by an automated script. - return {getDateCopy(uptime)}; + return {getDateCopy(uptime || "")}; }; const HostExpiration: React.FC = ({ expiration, noExpiration }) => { const getDateCopy = useDateFormat(); - // @ts-expect-error: FIXME. This comment was added by an automated script. - return {noExpiration ? DoesNotExpire : getDateCopy(expiration)}; + return ( + {noExpiration ? DoesNotExpire : getDateCopy(expiration || "")} + ); }; -const spawnHostCardFieldMaps = (sendEvent: SendEvent) => ({ +const spawnHostCardFieldMaps = ( + sendEvent: ExtractAnalyticsSendEvent, +) => ({ ID: (host: MyHost) => ( {host?.id} ( @@ -69,7 +67,7 @@ const spawnHostCardFieldMaps = (sendEvent: SendEvent) => ({ (tag) => tag.canBeModified && ( - {tag?.key}:{tag?.value} + {tag.key}:{tag.value} ), )} diff --git a/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/AddIssueModal/index.tsx b/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/AddIssueModal/index.tsx index 328706d18..c0d85b0d8 100644 --- a/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/AddIssueModal/index.tsx +++ b/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/AddIssueModal/index.tsx @@ -70,7 +70,7 @@ export const AddIssueModal: React.FC = ({ `There was an error adding the issue: ${error.message}`, ); }, - refetchQueries: ["AnnotationEventData"], + refetchQueries: ["SuspectedIssues", "Issues"], }); const spruceConfig = useSpruceConfig(); diff --git a/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/AnnotationNote.tsx b/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/AnnotationNote.tsx index dd962f69f..2739dbb20 100644 --- a/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/AnnotationNote.tsx +++ b/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/AnnotationNote.tsx @@ -48,7 +48,6 @@ const AnnotationNote: React.FC = ({ `There was an error updating this note: ${error.message}`, ); }, - refetchQueries: ["AnnotationEventData"], }); const saveAnnotationNote = () => { updateAnnotationNote({ diff --git a/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/BuildBaron.test.tsx b/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/BuildBaron.test.tsx index d49c031d9..3c4f5b783 100644 --- a/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/BuildBaron.test.tsx +++ b/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/BuildBaron.test.tsx @@ -1,7 +1,5 @@ import { RenderFakeToastContext } from "context/toast/__mocks__"; import { - AnnotationEventDataQuery, - AnnotationEventDataQueryVariables, BuildBaronCreateTicketMutation, BuildBaronCreateTicketMutationVariables, BuildBaronQuery, @@ -20,7 +18,6 @@ import { import { getUserMock } from "gql/mocks/getUser"; import { FILE_JIRA_TICKET } from "gql/mutations"; import { - ANNOTATION_EVENT_DATA, BUILD_BARON, CREATED_TICKETS, JIRA_CUSTOM_CREATED_ISSUES, @@ -318,35 +315,12 @@ const jiraIssuesMock: ApolloMock< }, }; -const annotationEventDataMock: ApolloMock< - AnnotationEventDataQuery, - AnnotationEventDataQueryVariables -> = { - request: { - query: ANNOTATION_EVENT_DATA, - variables: { - taskId, - execution, - }, - }, - result: { - data: { - task: { - id: taskId, - execution, - annotation: null, - }, - }, - }, -}; - const buildBaronMocks = [ customCreatedIssuesMock, fileJiraTicketMock, getBuildBaronMock, getJiraTicketsMock, getSpruceConfigMock, - annotationEventDataMock, getUserSettingsMock, getUserMock, jiraIssuesMock, diff --git a/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/Issues/AnnotationTickets.tsx b/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/Issues/AnnotationTickets.tsx index 86a6ea39e..8d8aa2834 100644 --- a/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/Issues/AnnotationTickets.tsx +++ b/apps/spruce/src/pages/task/taskTabs/buildBaronAndAnnotations/Issues/AnnotationTickets.tsx @@ -1,7 +1,6 @@ import { useState } from "react"; import styled from "@emotion/styled"; import Tooltip from "@leafygreen-ui/tooltip"; -import { useAnnotationAnalytics } from "analytics"; import { PlusButton } from "components/Buttons"; import { size } from "constants/tokens"; import { IssueLink } from "gql/generated/types"; @@ -30,7 +29,6 @@ const AnnotationTickets: React.FC = ({ tickets, userCanModify, }) => { - const annotationAnalytics = useAnnotationAnalytics(); const title = isIssue ? "Issues" : "Suspected Issues"; const buttonText = isIssue ? "Add Issue" : "Add Suspected Issue"; const [isAddAnnotationModalVisible, setIsAddAnnotationModalVisible] = @@ -38,11 +36,6 @@ const AnnotationTickets: React.FC = ({ const handleAdd = () => { setIsAddAnnotationModalVisible(true); - - annotationAnalytics.sendEvent({ - name: "Created task annotation", - type: isIssue ? "Issue" : "Suspected Issue", - }); }; return ( <> diff --git a/apps/spruce/src/utils/queryString/index.ts b/apps/spruce/src/utils/queryString/index.ts index a99997713..fd3306c9e 100644 --- a/apps/spruce/src/utils/queryString/index.ts +++ b/apps/spruce/src/utils/queryString/index.ts @@ -1,3 +1,2 @@ export * from "./parseQueryString"; export * from "./sortString"; -export * from "./stringifyQuery"; diff --git a/apps/spruce/src/utils/queryString/parseQueryString.ts b/apps/spruce/src/utils/queryString/parseQueryString.ts index 943051cde..845f46a7d 100644 --- a/apps/spruce/src/utils/queryString/parseQueryString.ts +++ b/apps/spruce/src/utils/queryString/parseQueryString.ts @@ -9,13 +9,6 @@ export const parseQueryString = (search: string): ParseQueryString => // @ts-expect-error: FIXME. This comment was added by an automated script. queryString.parse(search, { arrayFormat: "comma" }); -export const parseQueryStringAsValue = (search: string) => - queryString.parse(search, { - arrayFormat: "comma", - parseBooleans: true, - parseNumbers: true, - }); - export const getString = (param: string | string[] = ""): string => Array.isArray(param) ? param[0] : param; diff --git a/apps/spruce/src/utils/queryString/stringifyQuery.test.ts b/apps/spruce/src/utils/queryString/stringifyQuery.test.ts deleted file mode 100644 index 0752718f6..000000000 --- a/apps/spruce/src/utils/queryString/stringifyQuery.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { stringifyQuery, stringifyQueryAsValue } from "./stringifyQuery"; - -describe("stringifyQuery", () => { - it("should return an empty string for an empty object", () => { - const result = stringifyQuery({}); - expect(result).toBe(""); - }); - - it("should return a string with one key-value pair for an object with one property", () => { - const result = stringifyQuery({ foo: "bar" }); - expect(result).toBe("foo=bar"); - }); - - it("should handle objects with multiple properties", () => { - const result = stringifyQuery({ foo: "bar", baz: 42 }); - expect(result).toBe("baz=42&foo=bar"); - }); - - it("should handle array properties with comma notation", () => { - const result = stringifyQuery({ foo: ["bar", "baz"] }); - expect(result).toBe("foo=bar,baz"); - }); -}); - -describe("stringifyQueryAsValue", () => { - it("should skip null properties", () => { - const result = stringifyQueryAsValue({ foo: "bar", baz: null }); - expect(result).toBe("foo=bar"); - }); - - it("should handle objects with multiple properties", () => { - const result = stringifyQueryAsValue({ foo: "bar", baz: 42 }); - expect(result).toBe("baz=42&foo=bar"); - }); - - it("should handle array properties with comma notation", () => { - const result = stringifyQueryAsValue({ foo: ["bar", "baz"], qux: null }); - expect(result).toBe("foo=bar,baz"); - }); - it("should skip empty strings", () => { - const result = stringifyQueryAsValue({ foo: "", bar: null }); - expect(result).toBe(""); - }); - it("should preserve empty strings if skipEmptyString is passed in", () => { - let result = stringifyQueryAsValue( - { foo: "", bar: null }, - { skipEmptyString: false }, - ); - expect(result).toBe("foo="); - result = stringifyQueryAsValue( - { foo: "", bar: 21 }, - { skipEmptyString: false }, - ); - expect(result).toBe("bar=21&foo="); - }); -}); diff --git a/apps/spruce/src/utils/queryString/stringifyQuery.ts b/apps/spruce/src/utils/queryString/stringifyQuery.ts deleted file mode 100644 index b2aa38ab4..000000000 --- a/apps/spruce/src/utils/queryString/stringifyQuery.ts +++ /dev/null @@ -1,17 +0,0 @@ -import queryString, { StringifyOptions } from "query-string"; - -export const stringifyQuery = (object: { [key: string]: any }) => - queryString.stringify(object, { - arrayFormat: "comma", - }); - -export const stringifyQueryAsValue = ( - object: { [key: string]: any }, - options: StringifyOptions = {}, -) => - queryString.stringify(object, { - arrayFormat: "comma", - skipNull: true, - skipEmptyString: true, - ...options, - }); diff --git a/apps/spruce/src/utils/url/updateUrlQueryParam.ts b/apps/spruce/src/utils/url/updateUrlQueryParam.ts index 4d8c00e96..4b41bc4f4 100644 --- a/apps/spruce/src/utils/url/updateUrlQueryParam.ts +++ b/apps/spruce/src/utils/url/updateUrlQueryParam.ts @@ -1,5 +1,6 @@ import { NavigateFunction } from "react-router-dom"; -import { parseQueryString, stringifyQuery } from "utils/queryString"; +import { stringifyQuery } from "@evg-ui/lib/utils/query-string"; +import { parseQueryString } from "utils/queryString"; export const updateUrlQueryParam = ( urlSearchParam: string, diff --git a/packages/lib/README.md b/packages/lib/README.md new file mode 100644 index 000000000..e7971389d --- /dev/null +++ b/packages/lib/README.md @@ -0,0 +1,49 @@ +# `lib` Directory Readme + +## Overview + +The `lib` directory is designed to house shared code used across our two main +projects. This centralization of common components and utilities aims to improve +code reusability and maintainability, ensuring that updates and bug fixes can be +applied in a single location. + +## Best Practices for Managing Dependencies + +When moving shared code to the `lib` directory, it is essential to manage +dependencies correctly to avoid duplication and maintain consistency. + +### External Dependencies: + +- If the shared code in the `lib` directory has an external dependency that is + not directly used by the individual projects, it is best practice to remove + that dependency from the individual project folders and install it within the + `lib` folder. This approach centralizes the dependency management and avoids + redundancy. + +### Project-Specific Dependencies: + +- If the shared code depends on an external library that is also used directly + by the individual projects, the dependency should be installed in both the + `lib` and the respective project. This ensures that each project has the + necessary dependencies available, and any version-specific requirements are + respected. + +### Example + +Consider a scenario where both Project A and Project B utilize a utility +function from `lib/module1/file1.ts` that depends on an external library, +`external-lib`. + +- If `external-lib` is used exclusively by `lib/module1/file1.ts`, it should be + installed only in the `lib` directory. +- If `external-lib` is also used directly by Project A or Project B, it should + be installed in both the respective project and the `lib` directory. + +## Contribution Guidelines + +When contributing to the `lib` directory, please ensure that: + +1. All shared components and utilities are well-documented. +2. Dependencies are clearly defined and managed according to the above best + practices. +3. Changes are tested across all projects that utilize the shared code. diff --git a/packages/lib/package.json b/packages/lib/package.json index b99f87139..7cee9bca2 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -19,7 +19,8 @@ "@leafygreen-ui/emotion": "4.0.7", "@leafygreen-ui/icon": "11.28.0", "@leafygreen-ui/text-input": "13.0.2", - "@leafygreen-ui/tokens": "2.5.2" + "@leafygreen-ui/tokens": "2.5.2", + "query-string": "9.0.0" }, "devDependencies": { "@evg-ui/eslint-config": "*", diff --git a/packages/lib/src/analytics/hooks.ts b/packages/lib/src/analytics/hooks.ts new file mode 100644 index 000000000..7add4c2f2 --- /dev/null +++ b/packages/lib/src/analytics/hooks.ts @@ -0,0 +1,40 @@ +import { useCallback, useMemo } from "react"; +import { Analytics, ActionType, AnalyticsProperties } from "./types"; +import { addNewRelicPageAction } from "./utils"; + +/** + * + * This is simply a clever named type to help with the typing of the `useAnalyticsRoot` hook. + * `AnalyticsIdentifier` is a string that represents the object to send with an event to our analytics provider. + * @example - "Page Name" + * @example - "Component Name" + */ +type AnalyticsIdentifier = string; + +/** + * `useAnalyticsRoot` is a hook that returns a function to send an event to our analytics provider + * @param analyticsIdentifier - The identifier to send with the event this is typically the page name or component name we are tracking + * @param attributes - The additional properties that will be passed into our analytics event. + * @returns - The sendEvent function to send an event to our analytics provider + */ +export const useAnalyticsRoot = < + Action extends ActionType, + Identifier extends AnalyticsIdentifier, +>( + analyticsIdentifier: Identifier, + attributes: AnalyticsProperties = {}, +): Analytics => { + const sendEvent: Analytics["sendEvent"] = useCallback( + (action) => { + addNewRelicPageAction(action, { + // Map the identifier and attributes to the object field to maintain backwards compatibility + // with the previous implementation of the analytics provider + object: analyticsIdentifier, + ...attributes, + }); + }, + [analyticsIdentifier, attributes], + ); + + return useMemo(() => ({ sendEvent }), [sendEvent]); +}; diff --git a/packages/lib/src/analytics/types.ts b/packages/lib/src/analytics/types.ts new file mode 100644 index 000000000..01cced89f --- /dev/null +++ b/packages/lib/src/analytics/types.ts @@ -0,0 +1,52 @@ +/** + * `ActionTypePrefixes` is a union type of all the prefixes that can be used to create an `ActionType`. + */ +type ActionTypePrefixes = + | "Changed" + | "Clicked" + | "Created" + | "Deleted" + | "Redirected" + | "Filtered" + | "Saved" + | "Sorted" + | "Toggled" + | "Viewed" + | "Used" + | "System Event"; + +/** + * `ActionType` is a type that represents an action that can be sent to our analytics provider. + * @param name - The name of the action to send to our analytics provider + * @example - { name: "Clicked Button" } + * @example - { name: "Changed Input" } + */ +export interface ActionType { + name: `${ActionTypePrefixes}${string}`; +} + +/** + * `sendEvent` is the function call to send an analytics event. It requires an Action + */ +type sendEvent = (action: Action) => void; + +/** + * `AnalyticsProperties` is an object that represents the properties and additional metadata to send with an event to our analytics provider. + * + */ +export interface AnalyticsProperties { + [key: string]: string | number | boolean; +} + +/** + * `Analytics` is an object that represents the analytics provider and the function to send an event to the provider. + */ +export interface Analytics { + sendEvent: sendEvent; +} + +/** + * `ExtractAnalyticsSendEvent` is a utility type that can be used to extract the sendEvent function from an analytics hook. + */ +export type ExtractAnalyticsSendEvent Analytics> = + ReturnType["sendEvent"]; diff --git a/packages/lib/src/analytics/utils.test.ts b/packages/lib/src/analytics/utils.test.ts new file mode 100644 index 000000000..8847b4393 --- /dev/null +++ b/packages/lib/src/analytics/utils.test.ts @@ -0,0 +1,72 @@ +import { addNewRelicPageAction } from "./utils"; + +describe("addNewRelicPageAction", () => { + const originalConsoleDebug = console.debug; + const mockNewRelic = { + addPageAction: vi.fn(), + } as any; + + beforeEach(() => { + window.newrelic = mockNewRelic; + console.debug = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + console.debug = originalConsoleDebug; + }); + + it("should send the correct action to newrelic.addPageAction when newrelic is available", () => { + const action = { + name: "Clicked TestAction", + additionalProp: "additionalValue", + } as const; + const properties = { prop1: "value1", prop2: "value2" }; + + addNewRelicPageAction(action, properties); + + expect(mockNewRelic.addPageAction).toHaveBeenCalledWith( + "Clicked TestAction", + { + prop1: "value1", + prop2: "value2", + additionalProp: "additionalValue", + }, + ); + }); + + it("should log the event to the console when newrelic is not available", () => { + delete window.newrelic; + + const action = { + name: "Clicked TestAction", + additionalProp: "additionalValue", + } as const; + const properties = { prop1: "value1", prop2: "value2" }; + + addNewRelicPageAction(action, properties); + + expect(console.debug).toHaveBeenCalledWith("ANALYTICS EVENT ", { + name: "Clicked TestAction", + attributesToSend: { + prop1: "value1", + prop2: "value2", + additionalProp: "additionalValue", + }, + }); + }); + + it("should handle actions with no additional properties", () => { + const action = { name: "Clicked TestAction" } as const; + const properties = { prop1: "value1" }; + + addNewRelicPageAction(action, properties); + + expect(mockNewRelic.addPageAction).toHaveBeenCalledWith( + "Clicked TestAction", + { + prop1: "value1", + }, + ); + }); +}); diff --git a/packages/lib/src/analytics/utils.ts b/packages/lib/src/analytics/utils.ts new file mode 100644 index 000000000..40974b740 --- /dev/null +++ b/packages/lib/src/analytics/utils.ts @@ -0,0 +1,29 @@ +import { parseQueryString } from "../utils/query-string"; +import { ActionType, AnalyticsProperties } from "./types"; + +/** + * `addNewRelicPageAction` is a function that sends an event to our analytics provider + * @param action - The action to send to our analytics provider + * @param action.name - The name of the action to send to our analytics provider + * @param properties - The properties to send with the event + */ +export const addNewRelicPageAction = ( + { name, ...actionProps }: A, + properties: AnalyticsProperties, +) => { + const { newrelic } = window; + const { search } = window.location; + const attributesToSend = { + ...properties, + ...parseQueryString(search), + ...actionProps, + }; + + if (typeof newrelic !== "object") { + // These will only print when new relic is not available such as during local development + console.debug("ANALYTICS EVENT ", { name, attributesToSend }); + return; + } + + newrelic.addPageAction(name, attributesToSend); +}; diff --git a/packages/lib/src/utils/query-string/index.ts b/packages/lib/src/utils/query-string/index.ts new file mode 100644 index 000000000..5efb09c41 --- /dev/null +++ b/packages/lib/src/utils/query-string/index.ts @@ -0,0 +1,37 @@ +import queryString, { ParseOptions, StringifyOptions } from "query-string"; + +/** + * `parseQueryString` is a function that parses a query-string string into an object + * @param search - The search string to parse + * @param options - The options to use when parsing the query-string these are the same options as the query-string library + * @returns - The parsed object + */ +export const parseQueryString = ( + search: string, + options: ParseOptions = {}, +) => { + const parseOptions: ParseOptions = { + arrayFormat: "comma", + parseBooleans: true, + parseNumbers: true, + ...options, + }; + return queryString.parse(search, parseOptions); +}; + +/** + * `stringifyQuery` is a function that stringifies an object into a query-string string + * @param object - The object to stringify into a query-string + * @param options - The options to use when stringifying the object these are the same options as the query-string library + * @returns - The query-string string + */ +export const stringifyQuery = ( + object: { [key: string]: any }, + options: StringifyOptions = {}, +) => + queryString.stringify(object, { + arrayFormat: "comma", + skipEmptyString: true, + skipNull: true, + ...options, + }); diff --git a/packages/lib/src/utils/query-string/query-string.test.ts b/packages/lib/src/utils/query-string/query-string.test.ts new file mode 100644 index 000000000..052261057 --- /dev/null +++ b/packages/lib/src/utils/query-string/query-string.test.ts @@ -0,0 +1,101 @@ +import { parseQueryString, stringifyQuery } from "."; + +describe("query-string", () => { + describe("stringifyQuery", () => { + it("ignores null", () => { + expect(stringifyQuery({ a: "hello", b: null })).toBe("a=hello"); + }); + it("ignores emptyStrings", () => { + expect(stringifyQuery({ a: "hello", b: "" })).toBe("a=hello"); + }); + it("should preserve empty strings if skipEmptyString is passed in", () => { + let result = stringifyQuery( + { bar: null, foo: "" }, + { skipEmptyString: false }, + ); + expect(result).toBe("foo="); + result = stringifyQuery({ bar: 21, foo: "" }, { skipEmptyString: false }); + expect(result).toBe("bar=21&foo="); + }); + it("can handle empty input", () => { + expect(stringifyQuery({})).toBe(""); + }); + it("stringifies a boolean correctly", () => { + expect(stringifyQuery({ exists: true })).toBe("exists=true"); + }); + it("stringifies a number correctly", () => { + expect(stringifyQuery({ files: 23 })).toBe("files=23"); + }); + it("stringifies an array correctly", () => { + expect( + stringifyQuery({ statuses: ["passed", "failed", "running"] }), + ).toBe("statuses=passed,failed,running"); + }); + it("stringifies an object containing many fields correctly", () => { + expect( + stringifyQuery({ + exists: true, + files: 23, + statuses: ["passed", "failed", "running"], + variant: [1, 3, 5], + }), + ).toBe( + "exists=true&files=23&statuses=passed,failed,running&variant=1,3,5", + ); + }); + }); + + describe("parseQueryString", () => { + it("parses a single query param with a string", () => { + expect(parseQueryString("status=passed")).toMatchObject({ + status: "passed", + }); + }); + it("parses multiple query params that are strings", () => { + expect( + parseQueryString("status=passed&variant=ubuntu1604"), + ).toMatchObject({ + status: "passed", + variant: "ubuntu1604", + }); + }); + it("parses a query param with an array as a value", () => { + expect(parseQueryString("statuses=failed,passed,ehh")).toMatchObject({ + statuses: ["failed", "passed", "ehh"], + }); + }); + it("parses a query param with multiple arrays as value", () => { + expect( + parseQueryString( + "statuses=failed,passed,ehh&variants=ubuntu1604,GLADOS", + ), + ).toMatchObject({ + statuses: ["failed", "passed", "ehh"], + variants: ["ubuntu1604", "GLADOS"], + }); + }); + it("parses a query param with a mixed array and a single string as a value", () => { + expect( + parseQueryString("status=failed&variants=ubuntu1604,GLADOS"), + ).toMatchObject({ + status: "failed", + variants: ["ubuntu1604", "GLADOS"], + }); + }); + it("parses a query param with a boolean as a value", () => { + expect(parseQueryString("status=true")).toMatchObject({ + status: true, + }); + }); + it("parses a query param with a number as a value", () => { + expect(parseQueryString("status=1")).toMatchObject({ + status: 1, + }); + }); + it("parses a query param with a number array as a value", () => { + expect(parseQueryString("status=1,2,3")).toMatchObject({ + status: [1, 2, 3], + }); + }); + }); +}); diff --git a/packages/lib/src/vite-env.d.ts b/packages/lib/src/vite-env.d.ts new file mode 100644 index 000000000..392c081a1 --- /dev/null +++ b/packages/lib/src/vite-env.d.ts @@ -0,0 +1,10 @@ +interface Window { + newrelic?: { + addPageAction(name: string, attributes: object); + setCustomAttribute: ( + name: string, + value: string | number | boolean | null, + persist?: boolean, + ) => void; + }; +} diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index 564a59900..1c2ab4e50 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.base.json", - "include": ["src"] + "include": ["src"], + "compilerOptions": { + "baseUrl": "src" + } }