diff --git a/packages/tracing-internal/src/browser/index.ts b/packages/tracing-internal/src/browser/index.ts index d1bbbffe02b3..6d4168329dc4 100644 --- a/packages/tracing-internal/src/browser/index.ts +++ b/packages/tracing-internal/src/browser/index.ts @@ -15,5 +15,6 @@ export { addPerformanceInstrumentationHandler, addClsInstrumentationHandler, addFidInstrumentationHandler, + addTtfbInstrumentationHandler, addLcpInstrumentationHandler, } from './instrument'; diff --git a/packages/tracing-internal/src/browser/instrument.ts b/packages/tracing-internal/src/browser/instrument.ts index 2a4e7acaf3b1..35e251983508 100644 --- a/packages/tracing-internal/src/browser/instrument.ts +++ b/packages/tracing-internal/src/browser/instrument.ts @@ -5,10 +5,11 @@ import { onCLS } from './web-vitals/getCLS'; import { onFID } from './web-vitals/getFID'; import { onLCP } from './web-vitals/getLCP'; import { observe } from './web-vitals/lib/observe'; +import { onTTFB } from './web-vitals/onTTFB'; type InstrumentHandlerTypePerformanceObserver = 'longtask' | 'event' | 'navigation' | 'paint' | 'resource'; -type InstrumentHandlerTypeMetric = 'cls' | 'lcp' | 'fid'; +type InstrumentHandlerTypeMetric = 'cls' | 'lcp' | 'fid' | 'ttfb'; // We provide this here manually instead of relying on a global, as this is not available in non-browser environements // And we do not want to expose such types @@ -86,6 +87,7 @@ const instrumented: { [key in InstrumentHandlerType]?: boolean } = {}; let _previousCls: Metric | undefined; let _previousFid: Metric | undefined; let _previousLcp: Metric | undefined; +let _previousTtfb: Metric | undefined; /** * Add a callback that will be triggered when a CLS metric is available. @@ -123,6 +125,13 @@ export function addFidInstrumentationHandler(callback: (data: { metric: Metric } return addMetricObserver('fid', callback, instrumentFid, _previousFid); } +/** + * Add a callback that will be triggered when a FID metric is available. + */ +export function addTtfbInstrumentationHandler(callback: (data: { metric: Metric }) => void): CleanupHandlerCallback { + return addMetricObserver('ttfb', callback, instrumentTtfb, _previousTtfb); +} + export function addPerformanceInstrumentationHandler( type: 'event', callback: (data: { entries: (PerformanceEntry & { target?: unknown | null })[] }) => void, @@ -199,6 +208,15 @@ function instrumentLcp(): StopListening { }); } +function instrumentTtfb(): StopListening { + return onTTFB(metric => { + triggerHandlers('ttfb', { + metric, + }); + _previousTtfb = metric; + }); +} + function addMetricObserver( type: InstrumentHandlerTypeMetric, callback: InstrumentHandlerCallback, diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 4eabd49f218c..364a1617112b 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -11,10 +11,13 @@ import { addFidInstrumentationHandler, addLcpInstrumentationHandler, addPerformanceInstrumentationHandler, + addTtfbInstrumentationHandler, } from '../instrument'; import { WINDOW } from '../types'; +import { getNavigationEntry } from '../web-vitals/lib/getNavigationEntry'; import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher'; import type { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types'; +import type { TTFBMetric } from '../web-vitals/types/ttfb'; import { isMeasurementValue, startAndEndSpan } from './utils'; const MAX_INT_AS_BYTES = 2147483647; @@ -54,11 +57,13 @@ export function startTrackingWebVitals(): () => void { const fidCallback = _trackFID(); const clsCallback = _trackCLS(); const lcpCallback = _trackLCP(); + const ttfbCallback = _trackTtfb(); return (): void => { fidCallback(); clsCallback(); lcpCallback(); + ttfbCallback(); }; } @@ -173,6 +178,18 @@ function _trackFID(): () => void { }); } +function _trackTtfb(): () => void { + return addTtfbInstrumentationHandler(({ metric }) => { + const entry = metric.entries[metric.entries.length - 1]; + if (!entry) { + return; + } + + DEBUG_BUILD && logger.log('[Measurements] Adding TTFB'); + _measurements['ttfb'] = { value: metric.value, unit: 'millisecond' }; + }); +} + /** Add performance related spans to a span */ export function addPerformanceEntries(span: Span): void { const performance = getBrowserPerformanceAPI(); @@ -186,9 +203,6 @@ export function addPerformanceEntries(span: Span): void { const performanceEntries = performance.getEntries(); - let responseStartTimestamp: number | undefined; - let requestStartTimestamp: number | undefined; - const { op, start_timestamp: transactionStartTime } = spanToJSON(span); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -203,8 +217,6 @@ export function addPerformanceEntries(span: Span): void { switch (entry.entryType) { case 'navigation': { _addNavigationSpans(span, entry, timeOrigin); - responseStartTimestamp = timeOrigin + msToSec(entry.responseStart); - requestStartTimestamp = timeOrigin + msToSec(entry.requestStart); break; } case 'mark': @@ -242,7 +254,7 @@ export function addPerformanceEntries(span: Span): void { // Measurements are only available for pageload transactions if (op === 'pageload') { - _addTtfbToMeasurements(_measurements, responseStartTimestamp, requestStartTimestamp, transactionStartTime); + _addTtfbRequestTimeToMeasurements(_measurements); ['fcp', 'fp', 'lcp'].forEach(name => { if (!_measurements[name] || !transactionStartTime || timeOrigin >= transactionStartTime) { @@ -524,39 +536,19 @@ function setResourceEntrySizeData( } /** - * Add ttfb information to measurements + * Add ttfb request time information to measurements. * - * Exported for tests + * ttfb information is added via vendored web vitals library. */ -export function _addTtfbToMeasurements( - _measurements: Measurements, - responseStartTimestamp: number | undefined, - requestStartTimestamp: number | undefined, - transactionStartTime: number | undefined, -): void { - // Generate TTFB (Time to First Byte), which measured as the time between the beginning of the span and the - // start of the response in milliseconds - if (typeof responseStartTimestamp === 'number' && transactionStartTime) { - DEBUG_BUILD && logger.log('[Measurements] Adding TTFB'); - _measurements['ttfb'] = { - // As per https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStart, - // responseStart can be 0 if the request is coming straight from the cache. - // This might lead us to calculate a negative ttfb if we don't use Math.max here. - // - // This logic is the same as what is in the web-vitals library to calculate ttfb - // https://github.com/GoogleChrome/web-vitals/blob/2301de5015e82b09925238a228a0893635854587/src/onTTFB.ts#L92 - // TODO(abhi): We should use the web-vitals library instead of this custom calculation. - value: Math.max(responseStartTimestamp - transactionStartTime, 0) * 1000, +function _addTtfbRequestTimeToMeasurements(_measurements: Measurements): void { + const navEntry = getNavigationEntry() as TTFBMetric['entries'][number]; + const { responseStart, requestStart } = navEntry; + + if (requestStart <= responseStart) { + DEBUG_BUILD && logger.log('[Measurements] Adding TTFB Request Time'); + _measurements['ttfb.requestTime'] = { + value: responseStart - requestStart, unit: 'millisecond', }; - - if (typeof requestStartTimestamp === 'number' && requestStartTimestamp <= responseStartTimestamp) { - // Capture the time spent making the request and receiving the first byte of the response. - // This is the time between the start of the request and the start of the response in milliseconds. - _measurements['ttfb.requestTime'] = { - value: (responseStartTimestamp - requestStartTimestamp) * 1000, - unit: 'millisecond', - }; - } } } diff --git a/packages/tracing-internal/src/browser/web-vitals/README.md b/packages/tracing-internal/src/browser/web-vitals/README.md index 73a0a72c2e4b..c87dd69d55b7 100644 --- a/packages/tracing-internal/src/browser/web-vitals/README.md +++ b/packages/tracing-internal/src/browser/web-vitals/README.md @@ -12,6 +12,8 @@ Current vendored web vitals are: - LCP (Largest Contentful Paint) - FID (First Input Delay) - CLS (Cumulative Layout Shift) +- INP (Interaction to Next Paint) +- TTFB (Time to First Byte) ## Notable Changes from web-vitals library @@ -44,3 +46,11 @@ https://github.com/getsentry/sentry-javascript/pull/2964 https://github.com/getsentry/sentry-javascript/pull/2909 - Added support for FID (First Input Delay) and LCP (Largest Contentful Paint) + +https://github.com/getsentry/sentry-javascript/pull/9690 + +- Added support for INP (Interaction to Next Paint) + +TODO + +- Add support for TTFB (Time to First Byte) diff --git a/packages/tracing-internal/src/browser/web-vitals/onTTFB.ts b/packages/tracing-internal/src/browser/web-vitals/onTTFB.ts new file mode 100644 index 000000000000..946141107fa8 --- /dev/null +++ b/packages/tracing-internal/src/browser/web-vitals/onTTFB.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { WINDOW } from '../types'; +import { bindReporter } from './lib/bindReporter'; +import { getActivationStart } from './lib/getActivationStart'; +import { getNavigationEntry } from './lib/getNavigationEntry'; +import { initMetric } from './lib/initMetric'; +import type { ReportCallback, ReportOpts } from './types'; +import type { TTFBMetric } from './types/ttfb'; + +/** + * Runs in the next task after the page is done loading and/or prerendering. + * @param callback + */ +const whenReady = (callback: () => void): void => { + if (!WINDOW.document) { + return; + } + + if (WINDOW.document.prerendering) { + addEventListener('prerenderingchange', () => whenReady(callback), true); + } else if (WINDOW.document.readyState !== 'complete') { + addEventListener('load', () => whenReady(callback), true); + } else { + // Queue a task so the callback runs after `loadEventEnd`. + setTimeout(callback, 0); + } +}; + +/** + * Calculates the [TTFB](https://web.dev/time-to-first-byte/) value for the + * current page and calls the `callback` function once the page has loaded, + * along with the relevant `navigation` performance entry used to determine the + * value. The reported value is a `DOMHighResTimeStamp`. + * + * Note, this function waits until after the page is loaded to call `callback` + * in order to ensure all properties of the `navigation` entry are populated. + * This is useful if you want to report on other metrics exposed by the + * [Navigation Timing API](https://w3c.github.io/navigation-timing/). For + * example, the TTFB metric starts from the page's [time + * origin](https://www.w3.org/TR/hr-time-2/#sec-time-origin), which means it + * includes time spent on DNS lookup, connection negotiation, network latency, + * and server processing time. + */ +export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts): void => { + // Set defaults + // eslint-disable-next-line no-param-reassign + opts = opts || {}; + + // https://web.dev/ttfb/#what-is-a-good-ttfb-score + // const thresholds = [800, 1800]; + + const metric = initMetric('TTFB'); + const report = bindReporter(onReport, metric, opts.reportAllChanges); + + whenReady(() => { + const navEntry = getNavigationEntry() as TTFBMetric['entries'][number]; + + if (navEntry) { + // The activationStart reference is used because TTFB should be + // relative to page activation rather than navigation start if the + // page was prerendered. But in cases where `activationStart` occurs + // after the first byte is received, this time should be clamped at 0. + metric.value = Math.max(navEntry.responseStart - getActivationStart(), 0); + + // In some cases the value reported is negative or is larger + // than the current page time. Ignore these cases: + // https://github.com/GoogleChrome/web-vitals/issues/137 + // https://github.com/GoogleChrome/web-vitals/issues/162 + if (metric.value < 0 || metric.value > performance.now()) return; + + metric.entries = [navEntry]; + + report(true); + } + }); +}; diff --git a/packages/tracing-internal/src/browser/web-vitals/types/base.ts b/packages/tracing-internal/src/browser/web-vitals/types/base.ts index ea2764c8ea64..6d748d7843b4 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types/base.ts +++ b/packages/tracing-internal/src/browser/web-vitals/types/base.ts @@ -105,4 +105,4 @@ export interface ReportOpts { */ export type LoadState = 'loading' | 'dom-interactive' | 'dom-content-loaded' | 'complete'; -export type StopListening = () => void; +export type StopListening = undefined | void | (() => void); diff --git a/packages/tracing-internal/src/browser/web-vitals/types/ttfb.ts b/packages/tracing-internal/src/browser/web-vitals/types/ttfb.ts new file mode 100644 index 000000000000..86f1329ebee8 --- /dev/null +++ b/packages/tracing-internal/src/browser/web-vitals/types/ttfb.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Metric, ReportCallback } from './base'; +import type { NavigationTimingPolyfillEntry } from './polyfills'; + +/** + * A TTFB-specific version of the Metric object. + */ +export interface TTFBMetric extends Metric { + name: 'TTFB'; + entries: PerformanceNavigationTiming[] | NavigationTimingPolyfillEntry[]; +} + +/** + * An object containing potentially-helpful debugging information that + * can be sent along with the TTFB value for the current page visit in order + * to help identify issues happening to real-users in the field. + */ +export interface TTFBAttribution { + /** + * The total time from when the user initiates loading the page to when the + * DNS lookup begins. This includes redirects, service worker startup, and + * HTTP cache lookup times. + */ + waitingTime: number; + /** + * The total time to resolve the DNS for the current request. + */ + dnsTime: number; + /** + * The total time to create the connection to the requested domain. + */ + connectionTime: number; + /** + * The time time from when the request was sent until the first byte of the + * response was received. This includes network time as well as server + * processing time. + */ + requestTime: number; + /** + * The `PerformanceNavigationTiming` entry used to determine TTFB (or the + * polyfill entry in browsers that don't support Navigation Timing). + */ + navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry; +} + +/** + * A TTFB-specific version of the Metric object with attribution. + */ +export interface TTFBMetricWithAttribution extends TTFBMetric { + attribution: TTFBAttribution; +} + +/** + * A TTFB-specific version of the ReportCallback function. + */ +export interface TTFBReportCallback extends ReportCallback { + (metric: TTFBMetric): void; +} + +/** + * A TTFB-specific version of the ReportCallback function with attribution. + */ +export interface TTFBReportCallbackWithAttribution extends TTFBReportCallback { + (metric: TTFBMetricWithAttribution): void; +} diff --git a/packages/tracing-internal/src/index.ts b/packages/tracing-internal/src/index.ts index 98efc8dc1032..2dd4cf2f1768 100644 --- a/packages/tracing-internal/src/index.ts +++ b/packages/tracing-internal/src/index.ts @@ -22,6 +22,7 @@ export { addPerformanceInstrumentationHandler, addClsInstrumentationHandler, addFidInstrumentationHandler, + addTtfbInstrumentationHandler, addLcpInstrumentationHandler, } from './browser'; diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index 84b3950aeeec..b0015ca0a514 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -10,7 +10,6 @@ import { } from '@sentry/core'; import type { Span } from '@sentry/types'; import type { ResourceEntry } from '../../../src/browser/metrics'; -import { _addTtfbToMeasurements } from '../../../src/browser/metrics'; import { _addMeasureSpans, _addResourceSpans } from '../../../src/browser/metrics'; import { WINDOW } from '../../../src/browser/types'; import { TestClient, getDefaultClientOptions } from '../../utils/TestClient'; @@ -341,34 +340,6 @@ describe('_addResourceSpans', () => { }); }); -describe('_addTtfbToMeasurements', () => { - it('adds ttfb to measurements', () => { - const measurements = {}; - _addTtfbToMeasurements(measurements, 300, 200, 100); - expect(measurements).toEqual({ - ttfb: { - unit: 'millisecond', - value: 200000, - }, - 'ttfb.requestTime': { - unit: 'millisecond', - value: 100000, - }, - }); - }); - - it('does not add negative ttfb', () => { - const measurements = {}; - _addTtfbToMeasurements(measurements, 100, 200, 300); - expect(measurements).toEqual({ - ttfb: { - unit: 'millisecond', - value: 0, - }, - }); - }); -}); - const setGlobalLocation = (location: Location) => { // @ts-expect-error need to delete this in order to set to new value delete WINDOW.location; diff --git a/yarn.lock b/yarn.lock index 6644c3a1fca6..644e37903884 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6712,17 +6712,7 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history-5@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history@*": +"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -7112,15 +7102,7 @@ "@types/history" "^3" "@types/react" "*" -"@types/react-router-4@npm:@types/react-router@5.1.14": - version "5.1.14" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" - integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== - dependencies: - "@types/history" "*" - "@types/react" "*" - -"@types/react-router-5@npm:@types/react-router@5.1.14": +"@types/react-router-4@npm:@types/react-router@5.1.14", "@types/react-router-5@npm:@types/react-router@5.1.14": version "5.1.14" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== @@ -25305,7 +25287,7 @@ react-is@^18.0.0: dependencies: "@remix-run/router" "1.0.2" -"react-router-6@npm:react-router@6.3.0": +"react-router-6@npm:react-router@6.3.0", react-router@6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== @@ -25320,13 +25302,6 @@ react-router-dom@^6.2.2: history "^5.2.0" react-router "6.3.0" -react-router@6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" - integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== - dependencies: - history "^5.2.0" - react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" @@ -27806,7 +27781,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -27834,13 +27809,6 @@ strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -30487,16 +30455,7 @@ workerpool@^6.4.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.4.0.tgz#f8d5cfb45fde32fa3b7af72ad617c3369567a462" integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@7.0.0, wrap-ansi@^5.1.0, wrap-ansi@^6.2.0, wrap-ansi@^7.0.0, wrap-ansi@^8.1.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^5.1.0, wrap-ansi@^6.2.0, wrap-ansi@^7.0.0, wrap-ansi@^8.1.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==