diff --git a/packages/rum-core/src/browser/performanceObservable.spec.ts b/packages/rum-core/src/browser/performanceObservable.spec.ts index cab7eaa12f..5fdcc2141d 100644 --- a/packages/rum-core/src/browser/performanceObservable.spec.ts +++ b/packages/rum-core/src/browser/performanceObservable.spec.ts @@ -1,4 +1,4 @@ -import type { Subscription } from '@datadog/browser-core' +import type { Duration, Subscription } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' import { mockClock } from '@datadog/browser-core/test' import type { RumConfiguration } from '../domain/configuration' @@ -39,7 +39,7 @@ describe('performanceObservable', () => { expect(observableCallback).toHaveBeenCalledWith([jasmine.objectContaining({ name: allowedUrl })]) }) - it('should not notify forbidden performance resources', () => { + it('should not notify performance resources with forbidden url', () => { const { notifyPerformanceEntries } = mockPerformanceObserver() const performanceResourceObservable = createPerformanceObservable(configuration, { type: RumPerformanceEntryType.RESOURCE, @@ -50,6 +50,17 @@ describe('performanceObservable', () => { expect(observableCallback).not.toHaveBeenCalled() }) + it('should not notify performance resources with invalid duration', () => { + const { notifyPerformanceEntries } = mockPerformanceObserver() + const performanceResourceObservable = createPerformanceObservable(configuration, { + type: RumPerformanceEntryType.RESOURCE, + }) + performanceSubscription = performanceResourceObservable.subscribe(observableCallback) + + notifyPerformanceEntries([createPerformanceEntry(RumPerformanceEntryType.RESOURCE, { duration: -1 as Duration })]) + expect(observableCallback).not.toHaveBeenCalled() + }) + it('should notify buffered performance resources asynchronously', () => { const { notifyPerformanceEntries } = mockPerformanceObserver() // add the performance entry to the buffer diff --git a/packages/rum-core/src/browser/performanceObservable.ts b/packages/rum-core/src/browser/performanceObservable.ts index 6c73c52101..b0f105f315 100644 --- a/packages/rum-core/src/browser/performanceObservable.ts +++ b/packages/rum-core/src/browser/performanceObservable.ts @@ -1,7 +1,7 @@ import type { Duration, RelativeTime, TimeoutId } from '@datadog/browser-core' import { addEventListener, Observable, setTimeout, clearTimeout, monitor, includes } from '@datadog/browser-core' import type { RumConfiguration } from '../domain/configuration' -import { isAllowedRequestUrl } from '../domain/resource/resourceUtils' +import { hasValidResourceEntryDuration, isAllowedRequestUrl } from '../domain/resource/resourceUtils' type RumPerformanceObserverConstructor = new (callback: PerformanceObserverCallback) => RumPerformanceObserver @@ -283,5 +283,8 @@ function filterRumPerformanceEntries( } function isForbiddenResource(configuration: RumConfiguration, entry: RumPerformanceEntry) { - return entry.entryType === RumPerformanceEntryType.RESOURCE && !isAllowedRequestUrl(configuration, entry.name) + return ( + entry.entryType === RumPerformanceEntryType.RESOURCE && + (!isAllowedRequestUrl(configuration, entry.name) || !hasValidResourceEntryDuration(entry)) + ) } diff --git a/packages/rum-core/src/domain/resource/matchRequestTiming.spec.ts b/packages/rum-core/src/domain/resource/matchRequestResourceEntry.spec.ts similarity index 58% rename from packages/rum-core/src/domain/resource/matchRequestTiming.spec.ts rename to packages/rum-core/src/domain/resource/matchRequestResourceEntry.spec.ts index 733f538924..adcb2c3482 100644 --- a/packages/rum-core/src/domain/resource/matchRequestTiming.spec.ts +++ b/packages/rum-core/src/domain/resource/matchRequestResourceEntry.spec.ts @@ -6,9 +6,9 @@ import type { RumPerformanceResourceTiming } from '../../browser/performanceObse import { RumPerformanceEntryType } from '../../browser/performanceObservable' import type { RequestCompleteEvent } from '../requestCollection' -import { matchRequestTiming } from './matchRequestTiming' +import { matchRequestResourceEntry } from './matchRequestResourceEntry' -describe('matchRequestTiming', () => { +describe('matchRequestResourceEntry', () => { const FAKE_REQUEST: Partial = { startClocks: relativeToClocks(100 as RelativeTime), duration: 500 as Duration, @@ -23,52 +23,52 @@ describe('matchRequestTiming', () => { spyOn(performance, 'getEntriesByName').and.returnValue(entries) }) - it('should match single timing nested in the request ', () => { + it('should match single entry nested in the request ', () => { const entry = createPerformanceEntry(RumPerformanceEntryType.RESOURCE, { startTime: 200 as RelativeTime, duration: 300 as Duration, }) entries.push(entry) - const matchingTiming = matchRequestTiming(FAKE_REQUEST as RequestCompleteEvent) + const matchingEntry = matchRequestResourceEntry(FAKE_REQUEST as RequestCompleteEvent) - expect(matchingTiming).toEqual(entry.toJSON() as RumPerformanceResourceTiming) + expect(matchingEntry).toEqual(entry.toJSON() as RumPerformanceResourceTiming) }) - it('should match single timing nested in the request with error margin', () => { + it('should match single entry nested in the request with error margin', () => { const entry = createPerformanceEntry(RumPerformanceEntryType.RESOURCE, { startTime: 99 as RelativeTime, duration: 502 as Duration, }) entries.push(entry) - const matchingTiming = matchRequestTiming(FAKE_REQUEST as RequestCompleteEvent) + const matchingEntry = matchRequestResourceEntry(FAKE_REQUEST as RequestCompleteEvent) - expect(matchingTiming).toEqual(entry.toJSON() as RumPerformanceResourceTiming) + expect(matchingEntry).toEqual(entry.toJSON() as RumPerformanceResourceTiming) }) - it('should not match single timing outside the request ', () => { + it('should not match single entry outside the request ', () => { const entry = createPerformanceEntry(RumPerformanceEntryType.RESOURCE, { startTime: 0 as RelativeTime, duration: 300 as Duration, }) entries.push(entry) - const matchingTiming = matchRequestTiming(FAKE_REQUEST as RequestCompleteEvent) + const matchingEntry = matchRequestResourceEntry(FAKE_REQUEST as RequestCompleteEvent) - expect(matchingTiming).toEqual(undefined) + expect(matchingEntry).toEqual(undefined) }) - it('should discard already matched timings when multiple identical requests are done conurently', () => { + it('should discard already matched entries when multiple identical requests are done conurently', () => { const entry1 = createPerformanceEntry(RumPerformanceEntryType.RESOURCE, { startTime: 200 as RelativeTime, duration: 300 as Duration, }) entries.push(entry1) - const matchingTiming1 = matchRequestTiming(FAKE_REQUEST as RequestCompleteEvent) + const matchingEntry1 = matchRequestResourceEntry(FAKE_REQUEST as RequestCompleteEvent) - expect(matchingTiming1).toEqual(entry1.toJSON() as RumPerformanceResourceTiming) + expect(matchingEntry1).toEqual(entry1.toJSON() as RumPerformanceResourceTiming) const entry2 = createPerformanceEntry(RumPerformanceEntryType.RESOURCE, { startTime: 99 as RelativeTime, @@ -76,12 +76,12 @@ describe('matchRequestTiming', () => { }) entries.push(entry2) - const matchingTiming2 = matchRequestTiming(FAKE_REQUEST as RequestCompleteEvent) + const matchingEntry2 = matchRequestResourceEntry(FAKE_REQUEST as RequestCompleteEvent) - expect(matchingTiming2).toEqual(entry2.toJSON() as RumPerformanceResourceTiming) + expect(matchingEntry2).toEqual(entry2.toJSON() as RumPerformanceResourceTiming) }) - it('should not match two not following timings nested in the request ', () => { + it('should not match two not following entries nested in the request ', () => { const entry1 = createPerformanceEntry(RumPerformanceEntryType.RESOURCE, { startTime: 150 as RelativeTime, duration: 100 as Duration, @@ -92,12 +92,12 @@ describe('matchRequestTiming', () => { }) entries.push(entry1, entry2) - const matchingTiming = matchRequestTiming(FAKE_REQUEST as RequestCompleteEvent) + const matchingEntry = matchRequestResourceEntry(FAKE_REQUEST as RequestCompleteEvent) - expect(matchingTiming).toEqual(undefined) + expect(matchingEntry).toEqual(undefined) }) - it('should not match multiple timings nested in the request', () => { + it('should not match multiple entries nested in the request', () => { const entry1 = createPerformanceEntry(RumPerformanceEntryType.RESOURCE, { startTime: 100 as RelativeTime, duration: 50 as Duration, @@ -112,12 +112,24 @@ describe('matchRequestTiming', () => { }) entries.push(entry1, entry2, entry3) - const matchingTiming = matchRequestTiming(FAKE_REQUEST as RequestCompleteEvent) + const matchingEntry = matchRequestResourceEntry(FAKE_REQUEST as RequestCompleteEvent) - expect(matchingTiming).toEqual(undefined) + expect(matchingEntry).toEqual(undefined) }) - it('[without tolerant_resource_timings] should not match invalid timing nested in the request ', () => { + it('should not match entry with invalid duration', () => { + const entry = createPerformanceEntry(RumPerformanceEntryType.RESOURCE, { + duration: -1 as Duration, + }) + + entries.push(entry) + + const matchingEntry = matchRequestResourceEntry(FAKE_REQUEST as RequestCompleteEvent) + + expect(matchingEntry).toEqual(undefined) + }) + + it('[without tolerant_resource_timings] should not match invalid entry nested in the request ', () => { const entry = createPerformanceEntry(RumPerformanceEntryType.RESOURCE, { // fetchStart < startTime is invalid fetchStart: 0 as RelativeTime, @@ -126,12 +138,12 @@ describe('matchRequestTiming', () => { entries.push(entry) - const matchingTiming = matchRequestTiming(FAKE_REQUEST as RequestCompleteEvent) + const matchingEntry = matchRequestResourceEntry(FAKE_REQUEST as RequestCompleteEvent) - expect(matchingTiming).toEqual(undefined) + expect(matchingEntry).toEqual(undefined) }) - it('[with tolerant_resource_timings] should match invalid timing nested in the request ', () => { + it('[with tolerant_resource_timings] should match invalid entry nested in the request ', () => { mockExperimentalFeatures([ExperimentalFeature.TOLERANT_RESOURCE_TIMINGS]) const entry = createPerformanceEntry(RumPerformanceEntryType.RESOURCE, { // fetchStart < startTime is invalid @@ -141,8 +153,8 @@ describe('matchRequestTiming', () => { entries.push(entry) - const matchingTiming = matchRequestTiming(FAKE_REQUEST as RequestCompleteEvent) + const matchingEntry = matchRequestResourceEntry(FAKE_REQUEST as RequestCompleteEvent) - expect(matchingTiming).toBeDefined() + expect(matchingEntry).toBeDefined() }) }) diff --git a/packages/rum-core/src/domain/resource/matchRequestTiming.ts b/packages/rum-core/src/domain/resource/matchRequestResourceEntry.ts similarity index 87% rename from packages/rum-core/src/domain/resource/matchRequestTiming.ts rename to packages/rum-core/src/domain/resource/matchRequestResourceEntry.ts index 7be1e2ec37..b1b1ffa25f 100644 --- a/packages/rum-core/src/domain/resource/matchRequestTiming.ts +++ b/packages/rum-core/src/domain/resource/matchRequestResourceEntry.ts @@ -3,7 +3,7 @@ import { addDuration } from '@datadog/browser-core' import type { RumPerformanceResourceTiming } from '../../browser/performanceObservable' import type { RequestCompleteEvent } from '../requestCollection' import { WeakSet } from '../../browser/polyfills' -import { isValidEntry } from './resourceUtils' +import { hasValidResourceEntryDuration, hasValidResourceEntryTimings } from './resourceUtils' interface Timing { startTime: RelativeTime @@ -25,7 +25,7 @@ const alreadyMatchedEntries = new WeakSet() * - then, if a single timing match, return the timing * - otherwise we can't decide, return undefined */ -export function matchRequestTiming(request: RequestCompleteEvent) { +export function matchRequestResourceEntry(request: RequestCompleteEvent) { if (!performance || !('getEntriesByName' in performance)) { return } @@ -37,7 +37,7 @@ export function matchRequestTiming(request: RequestCompleteEvent) { const candidates = sameNameEntries .filter((entry) => !alreadyMatchedEntries.has(entry)) - .filter((entry) => isValidEntry(entry)) + .filter((entry) => hasValidResourceEntryDuration(entry) && hasValidResourceEntryTimings(entry)) .filter((entry) => isBetween( entry, diff --git a/packages/rum-core/src/domain/resource/resourceCollection.ts b/packages/rum-core/src/domain/resource/resourceCollection.ts index f708536c45..cb5c2e171e 100644 --- a/packages/rum-core/src/domain/resource/resourceCollection.ts +++ b/packages/rum-core/src/domain/resource/resourceCollection.ts @@ -24,13 +24,13 @@ import type { RequestCompleteEvent } from '../requestCollection' import type { PageStateHistory } from '../contexts/pageStateHistory' import { PageState } from '../contexts/pageStateHistory' import { createTraceIdentifier } from '../tracing/tracer' -import { matchRequestTiming } from './matchRequestTiming' +import { matchRequestResourceEntry } from './matchRequestResourceEntry' import { - computePerformanceResourceDetails, - computePerformanceResourceDuration, - computeResourceKind, - computeSize, - isRequestKind, + computeResourceEntryDetails, + computeResourceEntryDuration, + computeResourceEntryType, + computeResourceEntrySize, + isResourceEntryRequestType, isLongDataUrl, sanitizeDataUrl, } from './resourceUtils' @@ -54,7 +54,7 @@ export function startResourceCollection( buffered: true, }).subscribe((entries) => { for (const entry of entries) { - if (!isRequestKind(entry)) { + if (!isResourceEntryRequestType(entry)) { const rawEvent = processResourceEntry(entry, configuration) if (rawEvent) { lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, rawEvent) @@ -82,7 +82,7 @@ function processRequest( configuration: RumConfiguration, pageStateHistory: PageStateHistory ): RawRumEventCollectedData | undefined { - const matchingTiming = matchRequestTiming(request) + const matchingTiming = matchRequestResourceEntry(request) const startClocks = matchingTiming ? relativeToClocks(matchingTiming.startTime) : request.startClocks const tracingInfo = computeRequestTracingInfo(request, configuration) if (!configuration.trackResources && !tracingInfo) { @@ -91,7 +91,7 @@ function processRequest( const type = request.type === RequestType.XHR ? ResourceType.XHR : ResourceType.FETCH - const correspondingTimingOverrides = matchingTiming ? computePerformanceEntryMetrics(matchingTiming) : undefined + const correspondingTimingOverrides = matchingTiming ? computeResourceEntryMetrics(matchingTiming) : undefined const duration = computeRequestDuration(pageStateHistory, startClocks, request.duration) @@ -136,13 +136,13 @@ function processResourceEntry( configuration: RumConfiguration ): RawRumEventCollectedData | undefined { const startClocks = relativeToClocks(entry.startTime) - const tracingInfo = computeEntryTracingInfo(entry, configuration) + const tracingInfo = computeResourceEntryTracingInfo(entry, configuration) if (!configuration.trackResources && !tracingInfo) { return } - const type = computeResourceKind(entry) - const entryMetrics = computePerformanceEntryMetrics(entry) + const type = computeResourceEntryType(entry) + const entryMetrics = computeResourceEntryMetrics(entry) const resourceEvent = combine( { @@ -170,16 +170,16 @@ function processResourceEntry( } } -function computePerformanceEntryMetrics(timing: RumPerformanceResourceTiming) { - const { renderBlockingStatus } = timing +function computeResourceEntryMetrics(entry: RumPerformanceResourceTiming) { + const { renderBlockingStatus } = entry return { resource: assign( { - duration: computePerformanceResourceDuration(timing), + duration: computeResourceEntryDuration(entry), render_blocking_status: renderBlockingStatus, }, - computeSize(timing), - computePerformanceResourceDetails(timing) + computeResourceEntrySize(entry), + computeResourceEntryDetails(entry) ), } } @@ -198,7 +198,7 @@ function computeRequestTracingInfo(request: RequestCompleteEvent, configuration: } } -function computeEntryTracingInfo(entry: RumPerformanceResourceTiming, configuration: RumConfiguration) { +function computeResourceEntryTracingInfo(entry: RumPerformanceResourceTiming, configuration: RumConfiguration) { const hasBeenTraced = entry.traceId if (!hasBeenTraced) { return undefined diff --git a/packages/rum-core/src/domain/resource/resourceUtils.spec.ts b/packages/rum-core/src/domain/resource/resourceUtils.spec.ts index 8f7645cee8..c9f06bf808 100644 --- a/packages/rum-core/src/domain/resource/resourceUtils.spec.ts +++ b/packages/rum-core/src/domain/resource/resourceUtils.spec.ts @@ -5,9 +5,9 @@ import type { RumConfiguration } from '../configuration' import { validateAndBuildRumConfiguration } from '../configuration' import { MAX_ATTRIBUTE_VALUE_CHAR_LENGTH, - computePerformanceResourceDetails, - computePerformanceResourceDuration, - computeResourceKind, + computeResourceEntryDetails, + computeResourceEntryDuration, + computeResourceEntryType, isAllowedRequestUrl, isLongDataUrl, sanitizeDataUrl, @@ -35,7 +35,7 @@ function generateResourceWith(overrides: Partial) return completeTiming as RumPerformanceResourceTiming } -describe('computeResourceKind', () => { +describe('computeResourceEntryType', () => { ;[ { description: 'file extension with query params', @@ -72,16 +72,16 @@ describe('computeResourceKind', () => { }) => { it(`should compute resource kind: ${description}`, () => { const entry = generateResourceWith({ initiatorType, name }) - expect(computeResourceKind(entry)).toEqual(expected) + expect(computeResourceEntryType(entry)).toEqual(expected) }) } ) }) -describe('computePerformanceResourceDetails', () => { +describe('computeResourceEntryDetails', () => { it('should not compute entry without detailed timings', () => { expect( - computePerformanceResourceDetails( + computeResourceEntryDetails( generateResourceWith({ connectEnd: 0 as RelativeTime, connectStart: 0 as RelativeTime, @@ -97,8 +97,8 @@ describe('computePerformanceResourceDetails', () => { ).toBeUndefined() }) - it('should compute timings from entry', () => { - expect(computePerformanceResourceDetails(generateResourceWith({}))).toEqual({ + it('should compute details from entry', () => { + expect(computeResourceEntryDetails(generateResourceWith({}))).toEqual({ connect: { start: 5e6 as ServerDuration, duration: 2e6 as ServerDuration }, dns: { start: 3e6 as ServerDuration, duration: 1e6 as ServerDuration }, download: { start: 40e6 as ServerDuration, duration: 10e6 as ServerDuration }, @@ -110,7 +110,7 @@ describe('computePerformanceResourceDetails', () => { it('should not compute redirect timing when no redirect', () => { expect( - computePerformanceResourceDetails( + computeResourceEntryDetails( generateResourceWith({ fetchStart: 10 as RelativeTime, redirectEnd: 0 as RelativeTime, @@ -128,7 +128,7 @@ describe('computePerformanceResourceDetails', () => { it('should not compute dns timing when persistent connection or cache', () => { expect( - computePerformanceResourceDetails( + computeResourceEntryDetails( generateResourceWith({ domainLookupEnd: 12 as RelativeTime, domainLookupStart: 12 as RelativeTime, @@ -146,7 +146,7 @@ describe('computePerformanceResourceDetails', () => { it('should not compute ssl timing when no secure connection', () => { expect( - computePerformanceResourceDetails( + computeResourceEntryDetails( generateResourceWith({ secureConnectionStart: 0 as RelativeTime, }) @@ -162,7 +162,7 @@ describe('computePerformanceResourceDetails', () => { it('should not compute ssl timing when persistent connection', () => { expect( - computePerformanceResourceDetails( + computeResourceEntryDetails( generateResourceWith({ connectEnd: 12 as RelativeTime, connectStart: 12 as RelativeTime, @@ -181,7 +181,7 @@ describe('computePerformanceResourceDetails', () => { it('should not compute connect timing when persistent connection', () => { expect( - computePerformanceResourceDetails( + computeResourceEntryDetails( generateResourceWith({ connectEnd: 12 as RelativeTime, connectStart: 12 as RelativeTime, @@ -242,20 +242,20 @@ describe('computePerformanceResourceDetails', () => { }, ].forEach(({ reason, timing, ...overrides }) => { it(`[without tolerant-resource-timings] should not compute entry when ${reason}`, () => { - expect(computePerformanceResourceDetails(generateResourceWith(overrides))).toBeUndefined() + expect(computeResourceEntryDetails(generateResourceWith(overrides))).toBeUndefined() }) if (timing) { it(`[with tolerant-resource-timings] should not include the '${timing}' timing when ${reason}`, () => { mockExperimentalFeatures([ExperimentalFeature.TOLERANT_RESOURCE_TIMINGS]) - expect(computePerformanceResourceDetails(generateResourceWith(overrides))![timing]).toBeUndefined() + expect(computeResourceEntryDetails(generateResourceWith(overrides))![timing]).toBeUndefined() }) } }) it('should allow really fast document resource', () => { expect( - computePerformanceResourceDetails( + computeResourceEntryDetails( generateResourceWith({ connectEnd: 10 as RelativeTime, connectStart: 10 as RelativeTime, @@ -277,15 +277,13 @@ describe('computePerformanceResourceDetails', () => { }) }) -describe('computePerformanceResourceDuration', () => { +describe('computeResourceEntryDuration', () => { it('should return the entry duration', () => { - expect(computePerformanceResourceDuration(generateResourceWith({}))).toBe(50e6 as ServerDuration) + expect(computeResourceEntryDuration(generateResourceWith({}))).toBe(50e6 as ServerDuration) }) it('should use other available timing if the duration is 0', () => { - expect(computePerformanceResourceDuration(generateResourceWith({ duration: 0 as Duration }))).toBe( - 50e6 as ServerDuration - ) + expect(computeResourceEntryDuration(generateResourceWith({ duration: 0 as Duration }))).toBe(50e6 as ServerDuration) }) }) diff --git a/packages/rum-core/src/domain/resource/resourceUtils.ts b/packages/rum-core/src/domain/resource/resourceUtils.ts index f60234d2a8..8b30ffb5fa 100644 --- a/packages/rum-core/src/domain/resource/resourceUtils.ts +++ b/packages/rum-core/src/domain/resource/resourceUtils.ts @@ -13,16 +13,16 @@ import { import type { RumPerformanceResourceTiming } from '../../browser/performanceObservable' -import type { PerformanceResourceDetailsElement } from '../../rawRumEvent.types' +import type { ResourceEntryDetailsElement } from '../../rawRumEvent.types' import type { RumConfiguration } from '../configuration' -export interface PerformanceResourceDetails { - redirect?: PerformanceResourceDetailsElement - dns?: PerformanceResourceDetailsElement - connect?: PerformanceResourceDetailsElement - ssl?: PerformanceResourceDetailsElement - first_byte?: PerformanceResourceDetailsElement - download?: PerformanceResourceDetailsElement +export interface ResourceEntryDetails { + redirect?: ResourceEntryDetailsElement + dns?: ResourceEntryDetailsElement + connect?: ResourceEntryDetailsElement + ssl?: ResourceEntryDetailsElement + first_byte?: ResourceEntryDetailsElement + download?: ResourceEntryDetailsElement } export const FAKE_INITIAL_DOCUMENT = 'initial_document' @@ -47,15 +47,15 @@ const RESOURCE_TYPES: Array<[ResourceType, (initiatorType: string, path: string) ], ] -export function computeResourceKind(timing: RumPerformanceResourceTiming) { - const url = timing.name +export function computeResourceEntryType(entry: RumPerformanceResourceTiming) { + const url = entry.name if (!isValidUrl(url)) { - addTelemetryDebug(`Failed to construct URL for "${timing.name}"`) + addTelemetryDebug(`Failed to construct URL for "${entry.name}"`) return ResourceType.OTHER } const path = getPathName(url) for (const [type, isType] of RESOURCE_TYPES) { - if (isType(timing.initiatorType, path)) { + if (isType(entry.initiatorType, path)) { return type } } @@ -71,11 +71,11 @@ function areInOrder(...numbers: number[]) { return true } -export function isRequestKind(timing: RumPerformanceResourceTiming) { - return timing.initiatorType === 'xmlhttprequest' || timing.initiatorType === 'fetch' +export function isResourceEntryRequestType(entry: RumPerformanceResourceTiming) { + return entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch' } -export function computePerformanceResourceDuration(entry: RumPerformanceResourceTiming): ServerDuration { +export function computeResourceEntryDuration(entry: RumPerformanceResourceTiming): ServerDuration { const { duration, startTime, responseEnd } = entry // Safari duration is always 0 on timings blocked by cross origin policies. @@ -86,10 +86,8 @@ export function computePerformanceResourceDuration(entry: RumPerformanceResource return toServerDuration(duration) } -export function computePerformanceResourceDetails( - entry: RumPerformanceResourceTiming -): PerformanceResourceDetails | undefined { - if (!isValidEntry(entry)) { +export function computeResourceEntryDetails(entry: RumPerformanceResourceTiming): ResourceEntryDetails | undefined { + if (!hasValidResourceEntryTimings(entry)) { return undefined } const { @@ -107,7 +105,7 @@ export function computePerformanceResourceDetails( responseEnd, } = entry - const details: PerformanceResourceDetails = { + const details: ResourceEntryDetails = { download: formatTiming(startTime, responseStart, responseEnd), first_byte: formatTiming(startTime, requestStart, responseStart), } @@ -135,7 +133,17 @@ export function computePerformanceResourceDetails( return details } -export function isValidEntry(entry: RumPerformanceResourceTiming) { +/** + * Entries with negative duration are unexpected and should be dismissed. The intake will ignore RUM + * Resource events with negative durations anyway. + * Since Chromium 128, more entries have unexpected negative durations, see + * https://issues.chromium.org/issues/363031537 + */ +export function hasValidResourceEntryDuration(entry: RumPerformanceResourceTiming) { + return entry.duration >= 0 +} + +export function hasValidResourceEntryTimings(entry: RumPerformanceResourceTiming) { if (isExperimentalFeatureEnabled(ExperimentalFeature.TOLERANT_RESOURCE_TIMINGS)) { return true } @@ -175,7 +183,7 @@ function formatTiming(origin: RelativeTime, start: RelativeTime, end: RelativeTi } } -export function computeSize(entry: RumPerformanceResourceTiming) { +export function computeResourceEntrySize(entry: RumPerformanceResourceTiming) { // Make sure a request actually occurred if (entry.startTime < entry.responseStart) { const { encodedBodySize, decodedBodySize, transferSize } = entry diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index 33e4dcca3e..bf0a482b2a 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -43,12 +43,12 @@ export interface RawRumResourceEvent { decoded_body_size?: number transfer_size?: number render_blocking_status?: string - redirect?: PerformanceResourceDetailsElement - dns?: PerformanceResourceDetailsElement - connect?: PerformanceResourceDetailsElement - ssl?: PerformanceResourceDetailsElement - first_byte?: PerformanceResourceDetailsElement - download?: PerformanceResourceDetailsElement + redirect?: ResourceEntryDetailsElement + dns?: ResourceEntryDetailsElement + connect?: ResourceEntryDetailsElement + ssl?: ResourceEntryDetailsElement + first_byte?: ResourceEntryDetailsElement + download?: ResourceEntryDetailsElement } _dd: { trace_id?: string @@ -59,7 +59,7 @@ export interface RawRumResourceEvent { } } -export interface PerformanceResourceDetailsElement { +export interface ResourceEntryDetailsElement { duration: ServerDuration start: ServerDuration }