diff --git a/.dockerignore b/.dockerignore index c37ab8a5..a589c140 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,4 +7,5 @@ README.md !.next/static !.next/standalone .git -.sourcebot \ No newline at end of file +.sourcebot +.env.local \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 00000000..8bcb9623 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +NEXT_PUBLIC_POSTHOG_KEY=phc_VFn4CkEGHRdlVyOOw8mfkoj1DKVoG6y1007EClvzAnS +NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com +NEXT_PUBLIC_POSTHOG_ASSET_HOST=https://us-assets.i.posthog.com +NEXT_PUBLIC_POSTHOG_UI_HOST=https://us.posthog.com \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 976e6b92..90854fd6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,8 @@ RUN yarn config set network-timeout 1200000 RUN yarn --frozen-lockfile COPY . . ENV NEXT_TELEMETRY_DISABLED=1 +# @see: https://phase.dev/blog/nextjs-public-runtime-variables/ +ARG NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED=BAKED_NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED RUN yarn run build # ------ Runner ------ @@ -33,6 +35,9 @@ ENV DATA_DIR=/data ENV CONFIG_PATH=$DATA_DIR/config.json ENV DATA_CACHE_DIR=$DATA_DIR/.sourcebot +# Sourcebot collects anonymous usage data using [PostHog](https://posthog.com/). Uncomment this line to disable. +# ENV SOURCEBOT_TELEMETRY_DISABLED=1 + # Configure dependencies RUN apk add --no-cache git ca-certificates bind-tools tini jansson wget supervisor diff --git a/README.md b/README.md index 23085b23..c8d15de8 100644 --- a/README.md +++ b/README.md @@ -65,20 +65,26 @@ zoekt will now index your repositories (at `HEAD`). By default, it will re-index 4. Go to `http://localhost:3000` - once a index has been created, you should get results. - - ## Building Sourcebot TODO -## GitLab +## Disabling Telemetry + +By default, Sourcebot collects anonymous usage data using [PostHog](https://posthog.com/). You can disable this by setting the environment variable `SOURCEBOT_TELEMETRY_DISABLED` to `1` in the docker run command. Example: +```sh +docker run -e SOURCEBOT_TELEMETRY_DISABLED=1 ...stuff... ghcr.io/taqlaai/sourcebot:main +``` + + +# GitLab TODO -## BitBucket +# BitBucket TODO -### Todos +# Todos - Add instructions on using GitLab and BitBucket - Add instructions on building Sourcebot locally diff --git a/entrypoint.sh b/entrypoint.sh index 83beb30b..9b6e6fca 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,7 +1,6 @@ #!/bin/sh set -e - # Check if CONFIG_PATH is set if [ -z "$CONFIG_PATH" ]; then echo "\e[33mWarning: CONFIG_PATH environment variable is not set.\e[0m" @@ -35,4 +34,17 @@ else echo -e "\e[33mWarning: GitLab repositories will not be indexed since GITLAB_TOKEN was not set. If you are not using GitLab, disregard.\e[0m" fi +# Update nextjs public env variables w/o requiring a rebuild. +# @see: https://phase.dev/blog/nextjs-public-runtime-variables/ + +# Infer NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED if it is not set +if [ -z "$NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED" ] && [ ! -z "$SOURCEBOT_TELEMETRY_DISABLED" ]; then + export NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED="$SOURCEBOT_TELEMETRY_DISABLED" +fi + +find /app/public /app/.next -type f -name "*.js" | +while read file; do + sed -i "s|BAKED_NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED|${NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED}|g" "$file" +done + exec supervisord -c /etc/supervisor/conf.d/supervisord.conf \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 13928e00..a44d1189 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,26 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: "standalone" + output: "standalone", + + // @see : https://posthog.com/docs/advanced/proxy/nextjs + async rewrites() { + return [ + { + source: "/ingest/static/:path*", + destination: `${process.env.NEXT_PUBLIC_POSTHOG_ASSET_HOST}/static/:path*`, + }, + { + source: "/ingest/:path*", + destination: `${process.env.NEXT_PUBLIC_POSTHOG_HOST}/:path*`, + }, + { + source: "/ingest/decide", + destination: `${process.env.NEXT_PUBLIC_POSTHOG_HOST}/decide`, + }, + ]; + }, + // This is required to support PostHog trailing slash API requests + skipTrailingSlashRedirect: true, }; export default nextConfig; diff --git a/package.json b/package.json index e9bdc94d..2b66e969 100644 --- a/package.json +++ b/package.json @@ -27,17 +27,20 @@ "@tanstack/react-table": "^8.20.5", "@uiw/react-codemirror": "^4.23.0", "class-variance-authority": "^0.7.0", + "client-only": "^0.0.1", "clsx": "^2.1.1", "escape-string-regexp": "^5.0.0", "http-status-codes": "^2.3.0", "lucide-react": "^0.435.0", "next": "14.2.6", "next-themes": "^0.3.0", + "posthog-js": "^1.161.5", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.53.0", "react-hotkeys-hook": "^4.5.1", "react-resizable-panels": "^2.1.1", + "server-only": "^0.0.1", "sharp": "^0.33.5", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 29214f13..d698c6ec 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,9 +4,15 @@ import "./globals.css"; import { ThemeProvider } from "next-themes"; import { Suspense } from "react"; import { QueryClientProvider } from "./queryClientProvider"; +import { PHProvider } from "./posthogProvider"; +import dynamic from "next/dynamic"; const inter = Inter({ subsets: ["latin"] }); +const PostHogPageView = dynamic(() => import('./posthogPageView'), { + ssr: false, + }) + export const metadata: Metadata = { title: "Sourcebot", description: "Sourcebot", @@ -24,22 +30,25 @@ export default function RootLayout({ suppressHydrationWarning > - - - {/* - @todo : ideally we don't wrap everything in a suspense boundary. - @see : https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout - */} - - {children} - - - + + + + + {/* + @todo : ideally we don't wrap everything in a suspense boundary. + @see : https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout + */} + + {children} + + + + ); diff --git a/src/app/posthogPageView.tsx b/src/app/posthogPageView.tsx new file mode 100644 index 00000000..20f8d3a3 --- /dev/null +++ b/src/app/posthogPageView.tsx @@ -0,0 +1,28 @@ +'use client' + +import { usePathname, useSearchParams } from "next/navigation"; +import { useEffect } from "react"; +import { usePostHog } from 'posthog-js/react'; + +export default function PostHogPageView(): null { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const posthog = usePostHog(); + useEffect(() => { + // Track pageviews + if (pathname && posthog) { + let url = window.origin + pathname + if (searchParams.toString()) { + url = url + `?${searchParams.toString()}` + } + posthog.capture( + '$pageview', + { + '$current_url': url, + } + ) + } + }, [pathname, searchParams, posthog]) + + return null +} \ No newline at end of file diff --git a/src/app/posthogProvider.tsx b/src/app/posthogProvider.tsx new file mode 100644 index 00000000..22ffc745 --- /dev/null +++ b/src/app/posthogProvider.tsx @@ -0,0 +1,25 @@ +'use client' +import { NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_UI_HOST, NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED } from '@/lib/environment.client' +import posthog from 'posthog-js' +import { PostHogProvider } from 'posthog-js/react' + +if (typeof window !== 'undefined') { + if (!NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED) { + posthog.init(NEXT_PUBLIC_POSTHOG_KEY!, { + api_host: "/ingest", + ui_host: NEXT_PUBLIC_POSTHOG_UI_HOST, + person_profiles: 'identified_only', + capture_pageview: false, // Disable automatic pageview capture, as we capture manually + }); + } else { + console.log("PostHog telemetry disabled"); + } +} + +export function PHProvider({ + children, +}: { + children: React.ReactNode +}) { + return {children} +} \ No newline at end of file diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index e1993eb0..11ac4163 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -13,7 +13,7 @@ import { SymbolIcon } from "@radix-ui/react-icons"; import { useQuery } from "@tanstack/react-query"; import Image from "next/image"; import { useRouter } from "next/navigation"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import logoDark from "../../../public/sb_logo_dark.png"; import logoLight from "../../../public/sb_logo_light.png"; import { fetchFileSource, search } from "../api/(client)/client"; @@ -21,6 +21,7 @@ import { SearchBar } from "../searchBar"; import { SettingsDropdown } from "../settingsDropdown"; import { CodePreviewFile, CodePreviewPanel } from "./codePreviewPanel"; import { SearchResultsPanel } from "./searchResultsPanel"; +import useCaptureEvent from "@/hooks/useCaptureEvent"; const DEFAULT_NUM_RESULTS = 100; @@ -33,6 +34,8 @@ export default function SearchPage() { const [selectedMatchIndex, setSelectedMatchIndex] = useState(0); const [selectedFile, setSelectedFile] = useState(undefined); + const captureEvent = useCaptureEvent(); + const { data: searchResponse, isLoading } = useQuery({ queryKey: ["search", searchQuery, numResults], queryFn: () => search({ @@ -40,8 +43,41 @@ export default function SearchPage() { numResults, }), enabled: searchQuery.length > 0, + refetchOnWindowFocus: false, }); + useEffect(() => { + if (!searchResponse) { + return; + } + + const fileLanguages = searchResponse.Result.Files?.map(file => file.Language) || []; + + captureEvent("search_finished", { + contentBytesLoaded: searchResponse.Result.ContentBytesLoaded, + indexBytesLoaded: searchResponse.Result.IndexBytesLoaded, + crashes: searchResponse.Result.Crashes, + durationMs: searchResponse.Result.Duration / 1000000, + fileCount: searchResponse.Result.FileCount, + shardFilesConsidered: searchResponse.Result.ShardFilesConsidered, + filesConsidered: searchResponse.Result.FilesConsidered, + filesLoaded: searchResponse.Result.FilesLoaded, + filesSkipped: searchResponse.Result.FilesSkipped, + shardsScanned: searchResponse.Result.ShardsScanned, + shardsSkipped: searchResponse.Result.ShardsSkipped, + shardsSkippedFilter: searchResponse.Result.ShardsSkippedFilter, + matchCount: searchResponse.Result.MatchCount, + ngramMatches: searchResponse.Result.NgramMatches, + ngramLookups: searchResponse.Result.NgramLookups, + wait: searchResponse.Result.Wait, + matchTreeConstruction: searchResponse.Result.MatchTreeConstruction, + matchTreeSearch: searchResponse.Result.MatchTreeSearch, + regexpsConsidered: searchResponse.Result.RegexpsConsidered, + flushReason: searchResponse.Result.FlushReason, + fileLanguages, + }); + }, [captureEvent, searchResponse]); + const { fileMatches, searchDurationMs } = useMemo((): { fileMatches: SearchResultFile[], searchDurationMs: number } => { if (!searchResponse) { return { @@ -104,7 +140,7 @@ export default function SearchPage() { onClick={() => { const url = createPathWithQueryParams('/search', ["query", searchQuery], - ["numResults", `${numResults*2}`], + ["numResults", `${numResults * 2}`], ) router.push(url); }} diff --git a/src/hooks/useCaptureEvent.ts b/src/hooks/useCaptureEvent.ts new file mode 100644 index 00000000..70652afb --- /dev/null +++ b/src/hooks/useCaptureEvent.ts @@ -0,0 +1,26 @@ +'use client'; + +import { CaptureOptions } from "posthog-js"; +import posthog from "posthog-js"; +import { PosthogEvent, PosthogEventMap } from "../lib/posthogEvents"; + +export function captureEvent(event: E, properties: PosthogEventMap[E], options?: CaptureOptions) { + if(!options) { + options = {}; + } + options.send_instantly = true; + posthog.capture(event, properties, options); +} + +/** + * Captures a distinct action as a event and forwards it to the event service + * (i.e., PostHog). + * + * @returns A callback for capturing events. + * @see: https://posthog.com/docs/libraries/js#capturing-events + */ +const useCaptureEvent = () => { + return captureEvent; +} + +export default useCaptureEvent; \ No newline at end of file diff --git a/src/lib/environment.client.ts b/src/lib/environment.client.ts new file mode 100644 index 00000000..ba0d2904 --- /dev/null +++ b/src/lib/environment.client.ts @@ -0,0 +1,9 @@ +import 'client-only'; + +import { getEnv, getEnvBoolean } from "./utils"; + +export const NEXT_PUBLIC_POSTHOG_KEY = getEnv(process.env.NEXT_PUBLIC_POSTHOG_KEY); +export const NEXT_PUBLIC_POSTHOG_HOST = getEnv(process.env.NEXT_PUBLIC_POSTHOG_HOST); +export const NEXT_PUBLIC_POSTHOG_UI_HOST = getEnv(process.env.NEXT_PUBLIC_POSTHOG_UI_HOST); +export const NEXT_PUBLIC_POSTHOG_ASSET_HOST = getEnv(process.env.NEXT_PUBLIC_POSTHOG_ASSET_HOST); +export const NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED = getEnvBoolean(process.env.NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED, false); diff --git a/src/lib/environment.ts b/src/lib/environment.ts index d7717f10..be2a1386 100644 --- a/src/lib/environment.ts +++ b/src/lib/environment.ts @@ -1,14 +1,8 @@ +import 'server-only'; -const getEnv = (env: string | undefined, defaultValue = '') => { - return env ?? defaultValue; -} - -const getEnvNumber = (env: string | undefined, defaultValue: number = 0) => { - return Number(env) ?? defaultValue; -} +import { getEnv, getEnvNumber } from "./utils"; export const ZOEKT_WEBSERVER_URL = getEnv(process.env.ZOEKT_WEBSERVER_URL, "http://localhost:6070"); export const SHARD_MAX_MATCH_COUNT = getEnvNumber(process.env.SHARD_MAX_MATCH_COUNT, 10000); export const TOTAL_MAX_MATCH_COUNT = getEnvNumber(process.env.TOTAL_MAX_MATCH_COUNT, 100000); - export const NODE_ENV = process.env.NODE_ENV; diff --git a/src/lib/posthogEvents.ts b/src/lib/posthogEvents.ts new file mode 100644 index 00000000..b9c67c2f --- /dev/null +++ b/src/lib/posthogEvents.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/no-empty-object-type */ + +export type PosthogEventMap = { + search_finished: { + contentBytesLoaded: number, + indexBytesLoaded: number, + crashes: number, + durationMs: number, + fileCount: number, + shardFilesConsidered: number, + filesConsidered: number, + filesLoaded: number, + filesSkipped: number, + shardsScanned: number, + shardsSkipped: number, + shardsSkippedFilter: number, + matchCount: number, + ngramMatches: number, + ngramLookups: number, + wait: number, + matchTreeConstruction: number, + matchTreeSearch: number, + regexpsConsidered: number, + flushReason: number, + fileLanguages: string[] + } +} + +export type PosthogEvent = keyof PosthogEventMap; \ No newline at end of file diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index 5da6ec65..a6c32666 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -30,11 +30,34 @@ const rangeSchema = z.object({ End: locationSchema, }); +// @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L350 +export const searchResponseStats = { + ContentBytesLoaded: z.number(), + IndexBytesLoaded: z.number(), + Crashes: z.number(), + Duration: z.number(), + FileCount: z.number(), + ShardFilesConsidered: z.number(), + FilesConsidered: z.number(), + FilesLoaded: z.number(), + FilesSkipped: z.number(), + ShardsScanned: z.number(), + ShardsSkipped: z.number(), + ShardsSkippedFilter: z.number(), + MatchCount: z.number(), + NgramMatches: z.number(), + NgramLookups: z.number(), + Wait: z.number(), + MatchTreeConstruction: z.number(), + MatchTreeSearch: z.number(), + RegexpsConsidered: z.number(), + FlushReason: z.number(), +} + +// @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L497 export const searchResponseSchema = z.object({ Result: z.object({ - Duration: z.number(), - FileCount: z.number(), - MatchCount: z.number(), + ...searchResponseStats, Files: z.array(z.object({ FileName: z.string(), Repository: z.string(), @@ -72,7 +95,7 @@ export const fileSourceResponseSchema = z.object({ export type ListRepositoriesResponse = z.infer; // @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L728 -export const statsSchema = z.object({ +const repoStatsSchema = z.object({ Repos: z.number(), Shards: z.number(), Documents: z.number(), @@ -84,7 +107,7 @@ export const statsSchema = z.object({ }); // @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L716 -export const indexMetadataSchema = z.object({ +const indexMetadataSchema = z.object({ IndexFormatVersion: z.number(), IndexFeatureVersion: z.number(), IndexMinReaderVersion: z.number(), @@ -96,7 +119,7 @@ export const indexMetadataSchema = z.object({ }); // @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L555 -export const repositorySchema = z.object({ +const repositorySchema = z.object({ Name: z.string(), URL: z.string(), Source: z.string(), @@ -121,8 +144,8 @@ export const listRepositoriesResponseSchema = z.object({ Repos: z.array(z.object({ Repository: repositorySchema, IndexMetadata: indexMetadataSchema, - Stats: statsSchema, + Stats: repoStatsSchema, })), - Stats: statsSchema, + Stats: repoStatsSchema, }) }); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index ed507ed3..3eecc21c 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -81,3 +81,18 @@ export const isServiceError = (data: unknown): data is ServiceError => { 'errorCode' in data && 'message' in data; } + +export const getEnv = (env: string | undefined, defaultValue = '') => { + return env ?? defaultValue; +} + +export const getEnvNumber = (env: string | undefined, defaultValue: number = 0) => { + return Number(env) ?? defaultValue; +} + +export const getEnvBoolean = (env: string | undefined, defaultValue: boolean) => { + if (!env) { + return defaultValue; + } + return env === 'true' || env === '1'; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index bfca08f7..ac6add99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1268,7 +1268,7 @@ class-variance-authority@^0.7.0: dependencies: clsx "2.0.0" -client-only@0.0.1: +client-only@0.0.1, client-only@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== @@ -1897,6 +1897,11 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fflate@^0.4.8: + version "0.4.8" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" + integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2901,6 +2906,20 @@ postcss@^8, postcss@^8.4.23: picocolors "^1.0.1" source-map-js "^1.2.0" +posthog-js@^1.161.5: + version "1.161.5" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.161.5.tgz#3c07acf622c0719cd8e0e78ab4b0f3e85914c7ef" + integrity sha512-KGkb12grSQvGRauH6z+AUB83c4dgWqzmJFDjyMXarWRafaLN80HzjN1jk806x27HvdDXi21jtwiXekioWzEQ9g== + dependencies: + fflate "^0.4.8" + preact "^10.19.3" + web-vitals "^4.0.1" + +preact@^10.19.3: + version "10.24.0" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.24.0.tgz#bd8139bee35aafede3c6de96d2453982610dfeef" + integrity sha512-aK8Cf+jkfyuZ0ZZRG9FbYqwmEiGQ4y/PUO4SuTWoyWL244nZZh7bd5h2APd4rSNDYTBNghg1L+5iJN3Skxtbsw== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -3113,6 +3132,11 @@ semver@^7.5.4, semver@^7.6.0, semver@^7.6.3: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +server-only@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/server-only/-/server-only-0.0.1.tgz#0f366bb6afb618c37c9255a314535dc412cd1c9e" + integrity sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA== + set-function-length@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -3572,6 +3596,11 @@ w3c-keyname@^2.2.4: resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== +web-vitals@^4.0.1: + version "4.2.3" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.3.tgz#270c4baecfbc6ec6fc15da1989e465e5f9b94fb7" + integrity sha512-/CFAm1mNxSmOj6i0Co+iGFJ58OS4NRGVP+AWS/l509uIK5a1bSoIVaHz/ZumpHTfHSZBpgrJ+wjfpAOrTHok5Q== + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"