From bbfd7bb670e236e8c78152a433d2792cf7b3fc03 Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Mon, 26 Aug 2024 16:19:34 +0200 Subject: [PATCH] Playground improvement: scroll to json property INTER-775 (#153) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add developerTools smar signal * feat: add json link * feat: add highlights * feat: fix timeout conflicts * feat: add more links * feat: add more signals * feat: ip links + refactor * chore: fix error handling via Node SDK * test: add platwright test * chore: fix build * chore: remove only() * chore: review fixes * Update src/app/playground/components/ArrowLinks.tsx Co-authored-by: PrzemysΕ‚aw Ε»ydek * fix: scale arrow down --------- Co-authored-by: PrzemysΕ‚aw Ε»ydek --- e2e/playground.spec.ts | 5 + package.json | 2 +- src/app/playground/Playground.tsx | 161 ++++++++++++------ src/app/playground/components/ArrowLinks.tsx | 76 +++++++++ .../components/BotDetectionResult.tsx | 13 +- .../components/IpBlocklistResult.tsx | 19 +-- .../components/VpnDetectionResult.tsx | 9 +- .../playground/components/ipFormatUtils.tsx | 25 --- src/app/playground/playground.module.scss | 37 ++++ src/client/testIDs.ts | 1 + src/pages/api/event/[requestId].ts | 10 +- src/server/checks.ts | 2 +- yarn.lock | 8 +- 13 files changed, 257 insertions(+), 111 deletions(-) create mode 100644 src/app/playground/components/ArrowLinks.tsx delete mode 100644 src/app/playground/components/ipFormatUtils.tsx diff --git a/e2e/playground.spec.ts b/e2e/playground.spec.ts index 4f09adc0..fd62ba60 100644 --- a/e2e/playground.spec.ts +++ b/e2e/playground.spec.ts @@ -100,6 +100,11 @@ test.describe('Playground page', () => { expect(requestId).toHaveLength(20); expect(requestId).not.toEqual(oldRequestId); }); + + test('Clicking JSON link scrolls to appropriate JSON property', async ({ page }) => { + await page.getByText('See the JSON below').click({ force: true }); + await expect(page.locator('span.json-view--property:text("rawDeviceAttributes")')).toBeInViewport(); + }); }); test.describe('Proxy integration', () => { diff --git a/package.json b/package.json index 8c9c4587..cd4a58e0 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@fingerprintjs/fingerprintjs-pro-react": "^2.6.2", - "@fingerprintjs/fingerprintjs-pro-server-api": "^4.1.2", + "@fingerprintjs/fingerprintjs-pro-server-api": "^5.0.0", "@mui/icons-material": "^5.15.11", "@mui/material": "^5.15.11", "@radix-ui/react-accordion": "^1.1.2", diff --git a/src/app/playground/Playground.tsx b/src/app/playground/Playground.tsx index d321f69d..1aabd753 100644 --- a/src/app/playground/Playground.tsx +++ b/src/app/playground/Playground.tsx @@ -1,14 +1,13 @@ 'use client'; -import { FunctionComponent, useEffect, ReactNode } from 'react'; +import { useEffect, ReactNode } from 'react'; import { CollapsibleJsonViewer } from '../../client/components/common/CodeSnippet/CodeSnippet'; import dynamic from 'next/dynamic'; import SignalTable, { TableCellData } from './components/SignalTable'; -import BotDetectionResult from './components/BotDetectionResult'; +import botDetectionResult from './components/BotDetectionResult'; import { RefreshButton } from './components/RefreshButton'; -import IpBlocklistResult from './components/IpBlocklistResult'; -import VpnDetectionResult from './components/VpnDetectionResult'; -import { FormatIpAddress } from './components/ipFormatUtils'; +import { ipBlocklistResult } from './components/IpBlocklistResult'; +import { vpnDetectionResult } from './components/VpnDetectionResult'; import { usePlaygroundSignals } from './hooks/usePlaygroundSignals'; import { getLocationName, getZoomLevel } from '../../shared/utils/locationUtils'; import { FP_LOAD_OPTIONS } from '../../pages/_app'; @@ -21,7 +20,6 @@ import { FpjsProvider } from '@fingerprintjs/fingerprintjs-pro-react'; import Container from '../../client/components/common/Container'; import { TEST_IDS } from '../../client/testIDs'; import tableStyles from './components/SignalTable.module.scss'; -import { ExternalLinkArrowSvg } from '../../client/img/externalLinkArrowSvg'; import { HowToUseThisPlayground } from './components/HowToUseThisPlayground'; import classnames from 'classnames'; import { ResourceLinks } from '../../client/components/common/ResourceLinks/ResourceLinks'; @@ -33,28 +31,11 @@ import { import { ChevronSvg } from '../../client/img/chevronSvg'; import { pluralize } from '../../shared/utils'; import { motion } from 'framer-motion'; +import { JsonLink, DocsLink } from './components/ArrowLinks'; // Nothing magic about `8` here, each customer must define their own use-case specific threshold const SUSPECT_SCORE_RED_THRESHOLD = 8; -const DocsLink: FunctionComponent<{ children: string; href: string; style?: React.CSSProperties }> = ({ - children, - href, - style, -}) => { - const lastWord = children.split(' ').pop(); - const leadingWords = children.split(' ').slice(0, -1).join(' '); - return ( - - {leadingWords}{' '} - - {lastWord} - - - - ); -}; - const PLAYGROUND_COPY = { androidOnly: ( <> @@ -136,22 +117,53 @@ function Playground() { const identificationSignals: TableCellData[][] = [ [ { content: 'Browser' }, - { content: `${agentResponse?.browserName} ${agentResponse?.browserVersion}`, className: tableStyles.neutral }, + { + content: ( + + {`${agentResponse?.browserName} ${agentResponse?.browserVersion}`} + + ), + + className: tableStyles.neutral, + }, ], [ { content: 'Operating System' }, - { content: `${agentResponse?.os} ${agentResponse?.osVersion}`, className: tableStyles.neutral }, + { + content: ( + {`${agentResponse?.os} ${agentResponse?.osVersion}`} + ), + className: tableStyles.neutral, + }, ], [ { content: 'IP Address' }, - { content: , className: tableStyles.neutral }, + { + content: ( + + + {`${agentResponse?.ip}`} + + + ), + className: tableStyles.neutral, + }, ], [ { content: Last seen, }, { - content: agentResponse?.lastSeenAt.global ? timeAgoLabel(agentResponse?.lastSeenAt.global) : 'Unknown', + content: agentResponse?.lastSeenAt.global ? ( + + {timeAgoLabel(agentResponse?.lastSeenAt.global)} + + ) : ( + 'Unknown' + ), className: tableStyles.neutral, }, ], @@ -164,17 +176,23 @@ function Playground() { ], }, { - content: agentResponse?.confidence.score ? Math.trunc(agentResponse.confidence.score * 100) / 100 : 'N/A', + content: agentResponse?.confidence.score ? ( + + {String(Math.trunc(agentResponse.confidence.score * 100) / 100)} + + ) : ( + 'Not available' + ), className: agentResponse && agentResponse.confidence.score >= 0.7 ? tableStyles.green : tableStyles.red, }, ], ]; const suspectScore = usedIdentificationEvent?.products?.suspectScore?.data?.result; - // @ts-expect-error Not supported in Node SDK yet const remoteControl: boolean | undefined = usedIdentificationEvent?.products?.remoteControl?.data?.result; - // @ts-expect-error Not supported in Node SDK yet - const ipVelocity: number | undefined = usedIdentificationEvent?.products?.velocity?.data?.distinctIp.intervals['1h']; + + const ipVelocity: number | undefined = + usedIdentificationEvent?.products?.velocity?.data?.distinctIp?.intervals?.['1h']; const smartSignals: TableCellData[][] = [ [ @@ -184,7 +202,9 @@ function Playground() { Geolocation -
{getLocationName(ipLocation)}
+
+ {getLocationName(ipLocation)} +
), }, @@ -215,7 +235,11 @@ function Playground() { ), }, { - content: usedIdentificationEvent?.products?.incognito?.data?.result ? 'You are incognito πŸ•Ά' : 'Not detected', + content: ( + + {usedIdentificationEvent?.products?.incognito?.data?.result ? 'You are incognito πŸ•Ά' : 'Not detected'} + + ), className: usedIdentificationEvent?.products?.incognito?.data?.result ? tableStyles.red : tableStyles.green, }, ], @@ -228,7 +252,7 @@ function Playground() { ], }, { - content: , + content: {botDetectionResult({ event: usedIdentificationEvent })}, className: usedIdentificationEvent?.products?.botd?.data?.bot?.result === 'bad' ? tableStyles.red : tableStyles.green, }, @@ -242,7 +266,7 @@ function Playground() { ], }, { - content: , + content: {vpnDetectionResult({ event: usedIdentificationEvent })}, className: usedIdentificationEvent?.products?.vpn?.data?.result === true ? tableStyles.red : tableStyles.green, }, ], @@ -258,7 +282,12 @@ function Playground() { ], }, { - content: usedIdentificationEvent?.products?.tampering?.data?.result === true ? 'Yes πŸ–₯οΈπŸ”§' : 'Not detected', + content: ( + + {usedIdentificationEvent?.products?.tampering?.data?.result === true ? 'Yes πŸ–₯οΈπŸ”§' : 'Not detected'} + + ), + className: usedIdentificationEvent?.products?.tampering?.data?.result === true ? tableStyles.red : tableStyles.green, }, @@ -276,13 +305,11 @@ function Playground() { }, { content: ( - <> - {/* @ts-expect-error Not supported in Node SDK yet */} - {usedIdentificationEvent?.products?.developerTools?.data?.result === true ? 'Yes πŸ”§' : 'Not detected'}{' '} - + + {usedIdentificationEvent?.products?.developerTools?.data?.result === true ? 'Yes πŸ”§' : 'Not detected'} + ), className: - // @ts-expect-error Not supported in Node SDK yet usedIdentificationEvent?.products?.developerTools?.data?.result === true ? tableStyles.red : tableStyles.green, @@ -297,7 +324,12 @@ function Playground() { ), }, { - content: usedIdentificationEvent?.products?.virtualMachine?.data?.result === true ? 'Yes β˜οΈπŸ’»' : 'Not detected', + content: ( + + {usedIdentificationEvent?.products?.virtualMachine?.data?.result === true ? 'Yes β˜οΈπŸ’»' : 'Not detected'} + + ), + className: usedIdentificationEvent?.products?.virtualMachine?.data?.result === true ? tableStyles.red @@ -313,8 +345,11 @@ function Playground() { ), }, { - content: - usedIdentificationEvent?.products?.privacySettings?.data?.result === true ? 'Yes πŸ™ˆπŸ’»' : 'Not detected', + content: ( + + {usedIdentificationEvent?.products?.privacySettings?.data?.result === true ? 'Yes πŸ™ˆπŸ’»' : 'Not detected'} + + ), className: usedIdentificationEvent?.products?.privacySettings?.data?.result === true ? tableStyles.red @@ -333,7 +368,12 @@ function Playground() { ], }, { - content: remoteControl === undefined ? 'Not available' : remoteControl === true ? 'Yes πŸ•ΉοΈ' : 'Not detected', + content: + remoteControl === undefined ? ( + 'Not available' + ) : ( + {remoteControl === true ? 'Yes πŸ•ΉοΈ' : 'Not detected'} + ), className: remoteControl === undefined ? tableStyles.neutral @@ -354,7 +394,9 @@ function Playground() { ], }, { - content: , + content: ( + {ipBlocklistResult({ event: usedIdentificationEvent })} + ), className: usedIdentificationEvent?.products?.ipBlocklist?.data?.result || usedIdentificationEvent?.products?.proxy?.data?.result || @@ -375,7 +417,12 @@ function Playground() { ], }, { - content: usedIdentificationEvent?.products?.highActivity?.data?.result === true ? 'Yes πŸ”₯' : 'Not detected', + content: ( + + {usedIdentificationEvent?.products?.highActivity?.data?.result === true ? 'Yes πŸ”₯' : 'Not detected'} + + ), + className: usedIdentificationEvent?.products?.highActivity?.data?.result === true ? tableStyles.red : tableStyles.green, }, @@ -389,7 +436,11 @@ function Playground() { ], }, { - content: ipVelocity === undefined ? 'Not available' : `${pluralize(ipVelocity, 'IP')} in the past hour`, + content: ( + + {ipVelocity === undefined ? 'Not available' : `${pluralize(ipVelocity, 'IP')} in the past hour`} + + ), className: ipVelocity === undefined ? tableStyles.neutral : ipVelocity > 1 ? tableStyles.red : tableStyles.green, }, @@ -403,7 +454,12 @@ function Playground() { ], }, { - content: usedIdentificationEvent?.products?.suspectScore?.data?.result ?? 'Not available', + content: + suspectScore !== undefined ? ( + {String(suspectScore)} + ) : ( + 'Not available' + ), className: suspectScore === undefined ? tableStyles.neutral @@ -420,7 +476,10 @@ function Playground() { , ], }, - { content: 'See the JSON below', className: tableStyles.green }, + { + content: See the JSON below, + className: tableStyles.green, + }, ], ]; diff --git a/src/app/playground/components/ArrowLinks.tsx b/src/app/playground/components/ArrowLinks.tsx new file mode 100644 index 00000000..f84dec8f --- /dev/null +++ b/src/app/playground/components/ArrowLinks.tsx @@ -0,0 +1,76 @@ +import { FingerprintJSPro } from '@fingerprintjs/fingerprintjs-pro-react'; +import { EventResponse } from '@fingerprintjs/fingerprintjs-pro-server-api'; +import Link from 'next/link'; +import { FunctionComponent, useEffect, useRef } from 'react'; +import { ExternalLinkArrowSvg } from '../../../client/img/externalLinkArrowSvg'; +import styles from '../playground.module.scss'; +import { TEST_IDS } from '../../../client/testIDs'; + +export const DocsLink: FunctionComponent<{ children: string; href: string; style?: React.CSSProperties }> = ({ + children, + href, + style, +}) => { + // Prevent the arrow from being the only element on a new line + const words = children.split(' '); + const lastWord = [...words].pop(); + const leadingWords = [...words].slice(0, -1).join(' '); + return ( + + {leadingWords}{' '} + + {lastWord} + + + + ); +}; + +type PropertyName = keyof EventResponse['products'] | keyof FingerprintJSPro.ExtendedGetResult; + +export const JsonLink: FunctionComponent<{ + children: string; + propertyName: PropertyName; + elementOrder?: 'first' | 'last'; +}> = ({ children, propertyName, elementOrder }) => { + const timeout = useRef(); + + // Prevent the arrow from being the only element on a new line + const words = children.split(' '); + const lastWord = [...words].pop(); + const leadingWords = [...words].slice(0, -1).join(' '); + + // clear timeout when component unmounts + useEffect(() => () => clearTimeout(timeout.current), []); + + return ( +
{ + // scroll to property and highlight it + const jsonProperties = document.querySelectorAll('.json-view--property'); + const foundElements = Array.from(jsonProperties).filter((el) => el.textContent === propertyName); + const targetElement = elementOrder === 'first' ? foundElements[0] : foundElements[foundElements.length - 1]; + if (targetElement) { + if (targetElement.classList.contains(styles.jsonPropertyHighlighted)) { + targetElement.classList.remove(styles.jsonPropertyHighlighted); + clearTimeout(timeout.current); + } + targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); + targetElement.classList.add(styles.jsonPropertyHighlighted); + timeout.current = setTimeout(() => { + targetElement.classList.remove(styles.jsonPropertyHighlighted); + }, 5000); + } + }} + > + {leadingWords}{' '} + + {lastWord} + + +
+ ); +}; diff --git a/src/app/playground/components/BotDetectionResult.tsx b/src/app/playground/components/BotDetectionResult.tsx index b042afdc..edba144a 100644 --- a/src/app/playground/components/BotDetectionResult.tsx +++ b/src/app/playground/components/BotDetectionResult.tsx @@ -1,17 +1,16 @@ import { EventResponse } from '@fingerprintjs/fingerprintjs-pro-server-api'; -import { FunctionComponent } from 'react'; -const BotDetectionResult: FunctionComponent<{ event: EventResponse | undefined }> = ({ event }) => { +const botDetectionResult = ({ event }: { event: EventResponse | undefined }): string => { switch (event?.products?.botd?.data?.bot?.result) { case 'good': - return <>You are a good bot πŸ€–; + return 'You are a good bot πŸ€–'; case 'bad': - return <>You are a bad bot πŸ€– (type: {event?.products?.botd?.data?.bot?.type}); + return `You are a bad bot πŸ€– (type: ${event?.products?.botd?.data?.bot?.type})`; case 'notDetected': - return <>Not detected; + return 'Not detected'; default: - return <>Unknown; + return 'Unknown'; } }; -export default BotDetectionResult; +export default botDetectionResult; diff --git a/src/app/playground/components/IpBlocklistResult.tsx b/src/app/playground/components/IpBlocklistResult.tsx index 3ce6eda8..d51c0ca6 100644 --- a/src/app/playground/components/IpBlocklistResult.tsx +++ b/src/app/playground/components/IpBlocklistResult.tsx @@ -1,27 +1,24 @@ import { EventResponse } from '@fingerprintjs/fingerprintjs-pro-server-api'; -import { FunctionComponent } from 'react'; -const IpBlocklistResult: FunctionComponent<{ event: EventResponse | undefined }> = ({ event }) => { +export const ipBlocklistResult = ({ event }: { event: EventResponse | undefined }): string => { const blocklistData = event?.products?.ipBlocklist?.data; if (blocklistData?.details?.attackSource && blocklistData?.details?.emailSpam) { - return <>Your IP is on a blocklist 🚫 (it was part of multiple attacks); + return 'Your IP is on a blocklist 🚫 (it was part of multiple attacks)'; } if (blocklistData?.details?.attackSource) { - return <>Your IP is on a blocklist 🚫 (it was part of a network attack); + return 'Your IP is on a blocklist 🚫 (it was part of a network attack)'; } if (blocklistData?.details?.emailSpam) { - return <>Your IP is on a blocklist 🚫 (it was part of a spam attack); + return 'Your IP is on a blocklist 🚫 (it was part of a spam attack)'; } if (event?.products?.tor?.data?.result === true) { - return <>Your IP is a Tor exit node πŸ§…; + return 'Your IP is a Tor exit node πŸ§…'; } if (event?.products?.proxy?.data?.result === true) { - return <>Your IP is used by a public proxy provider πŸ”„; + return 'Your IP is used by a public proxy provider πŸ”„'; } if (blocklistData?.result === false) { - return <>Not detected; + return 'Not detected'; } - return <>Unknown; + return 'Unknown'; }; - -export default IpBlocklistResult; diff --git a/src/app/playground/components/VpnDetectionResult.tsx b/src/app/playground/components/VpnDetectionResult.tsx index d5cf542d..ac23e255 100644 --- a/src/app/playground/components/VpnDetectionResult.tsx +++ b/src/app/playground/components/VpnDetectionResult.tsx @@ -1,13 +1,12 @@ import { EventResponse } from '@fingerprintjs/fingerprintjs-pro-server-api'; -import { FunctionComponent } from 'react'; -const VpnDetectionResult: FunctionComponent<{ event: EventResponse | undefined }> = ({ event }) => { +export const vpnDetectionResult = ({ event }: { event: EventResponse | undefined }): string => { const VpnData = event?.products?.vpn?.data; if (!VpnData) { - return <>Signal not available; + return 'Signal not available'; } if (VpnData.result === false) { - return <>Not detected; + return 'Not detected'; } const reasons = [ VpnData.methods?.publicVPN ? 'public VPN IP' : undefined, @@ -17,5 +16,3 @@ const VpnDetectionResult: FunctionComponent<{ event: EventResponse | undefined } const reasonsString = reasons.length > 0 ? ` (${reasons.join(', ')})` : ''; return `You are using a VPN 🌐 ${reasonsString}`; }; - -export default VpnDetectionResult; diff --git a/src/app/playground/components/ipFormatUtils.tsx b/src/app/playground/components/ipFormatUtils.tsx deleted file mode 100644 index fafe2eb6..00000000 --- a/src/app/playground/components/ipFormatUtils.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { FunctionComponent } from 'react'; - -export const FormatIpAddress: FunctionComponent<{ ipAddress?: string }> = ({ ipAddress }) => { - if (!ipAddress) { - return null; - } - // Check if the string matches the IPv6 address pattern - const ipv6Regex = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i; - if (!ipv6Regex.test(ipAddress)) { - return {ipAddress}; // Not a valid IPv6 address, return the original string - } - - // Insert a line break in the middle of the IPv6 address - const middleIndex = ipAddress.length / 2; - const firstHalf = ipAddress.slice(0, middleIndex); - const secondHalf = ipAddress.slice(middleIndex); - - return ( - - {firstHalf} -
- {secondHalf} -
- ); -}; diff --git a/src/app/playground/playground.module.scss b/src/app/playground/playground.module.scss index c49dcb35..89a5a5c4 100644 --- a/src/app/playground/playground.module.scss +++ b/src/app/playground/playground.module.scss @@ -159,6 +159,26 @@ h2.sectionTitle { } } +.jsonLink { + &:hover { + color: v('orange-gradient'); + cursor: pointer; + + .jsonArrow { + translate: 0px 2px; + } + } + + .jsonArrow { + margin-left: 4px; + vertical-align: baseline; + width: 10px; + height: 10px; + transition: translate 0.2s ease-in-out; + transform: scaleY(0.85) rotate(135deg) ; + } +} + .title { color: v('dark-black'); font-family: Inter; @@ -319,3 +339,20 @@ h2.sectionTitle { grid-template-columns: minmax(0, 1fr); } } + +.ipAddress { + word-wrap: break-word; +} + +.jsonPropertyHighlighted { + animation: fadeBackground 5s ease-out; +} + +@keyframes fadeBackground { + 0% { + background-color: v('orange-gradient'); + } + 100% { + background-color: transparent; + } +} diff --git a/src/client/testIDs.ts b/src/client/testIDs.ts index 5dbe21d5..82400e70 100644 --- a/src/client/testIDs.ts +++ b/src/client/testIDs.ts @@ -88,6 +88,7 @@ export const TEST_IDS = { agentResponseJSON: 'agentResponseJSON', serverResponseJSON: 'serverResponseJSON', refreshButton: 'refreshButton', + jsonLink: 'jsonLink', }, } as const; diff --git a/src/pages/api/event/[requestId].ts b/src/pages/api/event/[requestId].ts index bb2f214a..5df2b8e5 100644 --- a/src/pages/api/event/[requestId].ts +++ b/src/pages/api/event/[requestId].ts @@ -63,7 +63,7 @@ async function tryGetFingerprintEvent( res.status(200).json(eventResponse); } catch (error) { // Retry only Not Found (404) requests. - if (isEventError(error) && error.status === 404 && retryCount > 1) { + if (isEventError(error) && error.statusCode === 404 && retryCount > 1) { setTimeout(() => tryGetFingerprintEvent(res, requestId, retryCount - 1, retryDelay), retryDelay); } else { console.error(error); @@ -74,10 +74,10 @@ async function tryGetFingerprintEvent( function sendErrorResponse(res: NextApiResponse, error: unknown) { if (isEventError(error)) { - res.statusMessage = `${error.error?.code} - ${error.error?.message}`; - res.status(error.status).json({ - message: error.error?.message, - code: error.error?.code, + res.statusMessage = `${error.errorCode} - ${error.message}`; + res.status(error.statusCode).json({ + message: error.message, + code: error.errorCode, }); } else { res.statusMessage = `Something went wrong ${error}`; diff --git a/src/server/checks.ts b/src/server/checks.ts index b5e72fbc..8c63f773 100644 --- a/src/server/checks.ts +++ b/src/server/checks.ts @@ -227,7 +227,7 @@ export const getAndValidateFingerprintResult = async ({ } catch (error) { console.error(error); // Throw a specific error if the request ID is not found - if (isEventError(error) && error.status === 404) { + if (isEventError(error) && error.statusCode === 404) { return { okay: false, error: 'Request ID not found, potential spoofing attack.' }; } return { okay: false, error: String(error) }; diff --git a/yarn.lock b/yarn.lock index b3863024..e0d710c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -512,10 +512,10 @@ "@fingerprintjs/fingerprintjs-pro-spa" "^1.2.0" fast-deep-equal "3.1.3" -"@fingerprintjs/fingerprintjs-pro-server-api@^4.1.2": - version "4.1.2" - resolved "https://registry.yarnpkg.com/@fingerprintjs/fingerprintjs-pro-server-api/-/fingerprintjs-pro-server-api-4.1.2.tgz#91d3a27d23f1191ee33f795c5595d151d9a69272" - integrity sha512-rKdVmYo/z6Fqkt5DHp9r6NYVtm/HuriCzja7lFOffBW7sNEVJ5iFXwlFnZ92Hp5n2k2fqcNaAM5cw5I7dZ5mnw== +"@fingerprintjs/fingerprintjs-pro-server-api@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@fingerprintjs/fingerprintjs-pro-server-api/-/fingerprintjs-pro-server-api-5.0.0.tgz#71f1747e6c66ca8e53e2113f6c1842adde685789" + integrity sha512-iqnq9L0jNuDplKwNMkc86N7254fQIv8ahguIWFXr8HbD1VS2jg/1iDzWLpgHj/vRyKkrHSlm3hyvGXy9Ld3Cjg== "@fingerprintjs/fingerprintjs-pro-spa@^1.2.0": version "1.3.0"