diff --git a/packages/rum/src/boot/rum.spec.ts b/packages/rum/src/boot/rum.spec.ts index e986a0648e..9818c08e0d 100644 --- a/packages/rum/src/boot/rum.spec.ts +++ b/packages/rum/src/boot/rum.spec.ts @@ -4,21 +4,15 @@ import { setup, TestSetupBuilder } from '../../test/specHelper' import { RumPerformanceNavigationTiming, RumPerformanceResourceTiming } from '../browser/performanceCollection' import { LifeCycle, LifeCycleEventType } from '../domain/lifeCycle' -import { RequestCompleteEvent } from '../domain/rumEventsCollection/requestCollection' +import { RequestCompleteEvent } from '../domain/requestCollection' import { ActionType, AutoUserAction } from '../domain/rumEventsCollection/userActionCollection' import { SESSION_KEEP_ALIVE_INTERVAL, THROTTLE_VIEW_UPDATE_PERIOD, View, } from '../domain/rumEventsCollection/viewCollection' -import { RumSession } from '../domain/rumSession' -import { RumEvent, RumResourceEvent, RumViewEvent } from '../index' -import { RawRumEvent } from '../types' -import { doGetInternalContext, handleResourceEntry, trackView } from './rum' - -function getEntry(handler: (startTime: number, event: RumEvent) => void, index: number) { - return (handler as jasmine.Spy).calls.argsFor(index)[1] as RumEvent -} +import { RumEvent, RumViewEvent } from '../index' +import { doGetInternalContext, trackView } from './rum' function getServerRequestBodies(server: sinon.SinonFakeServer) { return server.requests.map((r) => JSON.parse(r.requestBody) as T) @@ -28,13 +22,6 @@ function getRumMessage(server: sinon.SinonFakeServer, index: number) { return JSON.parse(server.requests[index].requestBody) as RumEvent } -function createMockSession(): RumSession { - return { - getId: () => 'foo', - isTracked: () => true, - isTrackedWithResource: () => true, - } -} interface ExpectedRequestBody { application_id: string date: number @@ -48,150 +35,6 @@ interface ExpectedRequestBody { } } -describe('rum handle performance entry', () => { - let handler: (startTime: number, event: RawRumEvent) => void - - beforeEach(() => { - if (isIE()) { - pending('no full rum support') - } - handler = jasmine.createSpy() - }) - - it('should handle resource when session track resource', () => { - const entry = { entryType: 'resource' as const, name: 'https://resource.com/valid' } - const session = createMockSession() - - handleResourceEntry(new LifeCycle(), session, handler, entry as RumPerformanceResourceTiming) - - expect(handler).toHaveBeenCalled() - }) - - it('should not handle resource when session does not track resource', () => { - const entry = { entryType: 'resource' as const, name: 'https://resource.com/valid' } - const session = createMockSession() - session.isTrackedWithResource = () => false - - handleResourceEntry(new LifeCycle(), session, handler, entry as RumPerformanceResourceTiming) - - expect(handler).not.toHaveBeenCalled() - }) - ;[ - { - description: 'file extension with query params', - expected: 'js', - url: 'http://localhost/test.js?from=foo.css', - }, - { - description: 'css extension', - expected: 'css', - url: 'http://localhost/test.css', - }, - { - description: 'image initiator', - expected: 'image', - initiatorType: 'img', - url: 'http://localhost/test', - }, - { - description: 'image extension', - expected: 'image', - url: 'http://localhost/test.jpg', - }, - ].forEach( - ({ - description, - url, - initiatorType, - expected, - }: { - description: string - url: string - initiatorType?: string - expected: string - }) => { - it(`should compute resource kind: ${description}`, () => { - const entry: Partial = { initiatorType, name: url, entryType: 'resource' } - - handleResourceEntry(new LifeCycle(), createMockSession(), handler, entry as RumPerformanceResourceTiming) - const resourceEvent = getEntry(handler, 0) as RumResourceEvent - expect(resourceEvent.resource.kind).toEqual(expected) - }) - } - ) - - it('should compute timing durations', () => { - const entry: Partial = { - connectEnd: 10, - connectStart: 3, - domainLookupEnd: 3, - domainLookupStart: 3, - entryType: 'resource', - name: 'http://localhost/test', - redirectEnd: 0, - redirectStart: 0, - requestStart: 20, - responseEnd: 100, - responseStart: 25, - secureConnectionStart: 0, - } - - handleResourceEntry(new LifeCycle(), createMockSession(), handler, entry as RumPerformanceResourceTiming) - const resourceEvent = getEntry(handler, 0) as RumResourceEvent - expect(resourceEvent.http.performance!.connect!.duration).toEqual(7 * 1e6) - expect(resourceEvent.http.performance!.download!.duration).toEqual(75 * 1e6) - }) - - describe('ignore invalid performance entry', () => { - it('when it has a negative timing start', () => { - const entry: Partial = { - connectEnd: 10, - connectStart: -3, - domainLookupEnd: 10, - domainLookupStart: 10, - entryType: 'resource', - fetchStart: 10, - name: 'http://localhost/test', - redirectEnd: 0, - redirectStart: 0, - requestStart: 10, - responseEnd: 100, - responseStart: 25, - secureConnectionStart: 0, - } - - handleResourceEntry(new LifeCycle(), createMockSession(), handler, entry as RumPerformanceResourceTiming) - const resourceEvent = getEntry(handler, 0) as RumResourceEvent - expect(resourceEvent.http.performance).toBe(undefined) - }) - - it('when it has timing start after its end', () => { - const entry: Partial = { - entryType: 'resource', - name: 'http://localhost/test', - responseEnd: 25, - responseStart: 100, - } - - handleResourceEntry(new LifeCycle(), createMockSession(), handler, entry as RumPerformanceResourceTiming) - const resourceEvent = getEntry(handler, 0) as RumResourceEvent - expect(resourceEvent.http.performance).toBe(undefined) - }) - }) - - it('should pass the traceId to the generated RumEvent', () => { - const entry: Partial = { - entryType: 'resource', - name: 'http://localhost/test', - traceId: '123', - } - - handleResourceEntry(new LifeCycle(), createMockSession(), handler, entry as RumPerformanceResourceTiming) - const resourceEvent = getEntry(handler, 0) as RumResourceEvent - expect(resourceEvent._dd!.traceId).toBe('123') - }) -}) - describe('rum view', () => { it('should convert timings to nanosecond', () => { const FAKE_VIEW = { @@ -243,115 +86,6 @@ describe('rum session', () => { setupBuilder.cleanup() }) - it('when tracked with resources should enable full tracking', () => { - const { server, stubBuilder, lifeCycle } = setupBuilder - .withPerformanceObserverStubBuilder() - .withPerformanceCollection() - .build() - - server.requests = [] - - stubBuilder.fakeEntry(FAKE_RESOURCE as PerformanceEntry, 'resource') - lifeCycle.notify(LifeCycleEventType.ERROR_COLLECTED, FAKE_ERROR as ErrorMessage) - lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, FAKE_REQUEST as RequestCompleteEvent) - lifeCycle.notify(LifeCycleEventType.CUSTOM_ACTION_COLLECTED, FAKE_CUSTOM_USER_ACTION_EVENT) - - expect(server.requests.length).toEqual(4) - }) - - it('when tracked without resources should not track resources', () => { - const { server, stubBuilder, lifeCycle } = setupBuilder - .withSession({ - getId: () => '1234', - isTracked: () => true, - isTrackedWithResource: () => false, - }) - .withPerformanceObserverStubBuilder() - .withPerformanceCollection() - .build() - - server.requests = [] - - stubBuilder.fakeEntry(FAKE_RESOURCE as PerformanceEntry, 'resource') - lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, FAKE_REQUEST as RequestCompleteEvent) - expect(server.requests.length).toEqual(0) - - lifeCycle.notify(LifeCycleEventType.ERROR_COLLECTED, FAKE_ERROR as ErrorMessage) - expect(server.requests.length).toEqual(1) - }) - - it('when not tracked should disable tracking', () => { - const { server, stubBuilder, lifeCycle } = setupBuilder - .withSession({ - getId: () => undefined, - isTracked: () => false, - isTrackedWithResource: () => false, - }) - .withPerformanceObserverStubBuilder() - .withPerformanceCollection() - .build() - - server.requests = [] - - stubBuilder.fakeEntry(FAKE_RESOURCE as PerformanceEntry, 'resource') - lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, FAKE_REQUEST as RequestCompleteEvent) - lifeCycle.notify(LifeCycleEventType.ERROR_COLLECTED, FAKE_ERROR as ErrorMessage) - lifeCycle.notify(LifeCycleEventType.CUSTOM_ACTION_COLLECTED, FAKE_CUSTOM_USER_ACTION_EVENT) - - expect(server.requests.length).toEqual(0) - }) - - it('when type change should enable/disable existing resource tracking', () => { - let isTracked = true - const { server, stubBuilder } = setupBuilder - .withSession({ - getId: () => '1234', - isTracked: () => isTracked, - isTrackedWithResource: () => isTracked, - }) - .withPerformanceObserverStubBuilder() - .withPerformanceCollection() - .build() - - server.requests = [] - - stubBuilder.fakeEntry(FAKE_RESOURCE as PerformanceEntry, 'resource') - expect(server.requests.length).toEqual(1) - - isTracked = false - stubBuilder.fakeEntry(FAKE_RESOURCE as PerformanceEntry, 'resource') - expect(server.requests.length).toEqual(1) - - isTracked = true - stubBuilder.fakeEntry(FAKE_RESOURCE as PerformanceEntry, 'resource') - expect(server.requests.length).toEqual(2) - }) - - it('when type change should enable/disable existing request tracking', () => { - let isTrackedWithResource = true - const { server, lifeCycle } = setupBuilder - .withSession({ - getId: () => '1234', - isTracked: () => true, - isTrackedWithResource: () => isTrackedWithResource, - }) - .withPerformanceCollection() - .build() - - server.requests = [] - - lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, FAKE_REQUEST as RequestCompleteEvent) - expect(server.requests.length).toEqual(1) - - isTrackedWithResource = false - lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, FAKE_REQUEST as RequestCompleteEvent) - expect(server.requests.length).toEqual(1) - - isTrackedWithResource = true - lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, FAKE_REQUEST as RequestCompleteEvent) - expect(server.requests.length).toEqual(2) - }) - it('when the session is renewed, a new view event should be sent', () => { let sessionId = '42' const { server, lifeCycle } = setupBuilder diff --git a/packages/rum/src/boot/rum.ts b/packages/rum/src/boot/rum.ts index 7e1fad1edd..4d592f2492 100644 --- a/packages/rum/src/boot/rum.ts +++ b/packages/rum/src/boot/rum.ts @@ -4,29 +4,19 @@ import { Configuration, Context, ErrorMessage, - generateUUID, getTimestamp, - includes, msToNs, - RequestType, - ResourceType, withSnakeCaseKeys, } from '@datadog/browser-core' import { startDOMMutationCollection } from '../browser/domMutationCollection' -import { RumPerformanceResourceTiming, startPerformanceCollection } from '../browser/performanceCollection' +import { startPerformanceCollection } from '../browser/performanceCollection' import { startRumAssembly } from '../domain/assembly' import { startRumAssemblyV2 } from '../domain/assemblyV2' import { LifeCycle, LifeCycleEventType } from '../domain/lifeCycle' import { ParentContexts, startParentContexts } from '../domain/parentContexts' -import { startLongTaskCollection } from '../domain/rumEventsCollection/longTaskCollection' -import { matchRequestTiming } from '../domain/rumEventsCollection/matchRequestTiming' -import { RequestCompleteEvent, startRequestCollection } from '../domain/rumEventsCollection/requestCollection' -import { - computePerformanceResourceDetails, - computePerformanceResourceDuration, - computeResourceKind, - computeSize, -} from '../domain/rumEventsCollection/resourceUtils' +import { startRequestCollection } from '../domain/requestCollection' +import { startLongTaskCollection } from '../domain/rumEventsCollection/longTask/longTaskCollection' +import { startResourceCollection } from '../domain/rumEventsCollection/resource/resourceCollection' import { CustomUserAction, startUserActionCollection } from '../domain/rumEventsCollection/userActionCollection' import { startViewCollection } from '../domain/rumEventsCollection/viewCollection' import { RumSession, startRumSession } from '../domain/rumSession' @@ -36,8 +26,6 @@ import { RawRumEvent, RumErrorEvent, RumEventCategory, - RumLongTaskEvent, - RumResourceEvent, RumUserActionEvent, RumViewEvent, } from '../types' @@ -121,8 +109,9 @@ export function startRumEventCollection( const batch = startRumBatch(configuration, lifeCycle) startRumAssembly(applicationId, configuration, lifeCycle, session, parentContexts, getGlobalContext) startRumAssemblyV2(applicationId, configuration, lifeCycle, session, parentContexts, getGlobalContext) - trackRumEvents(lifeCycle, session) + trackRumEvents(lifeCycle) startLongTaskCollection(lifeCycle, configuration) + startResourceCollection(lifeCycle, configuration, session) startViewCollection(location, lifeCycle) return { @@ -136,7 +125,7 @@ export function startRumEventCollection( } } -export function trackRumEvents(lifeCycle: LifeCycle, session: RumSession) { +export function trackRumEvents(lifeCycle: LifeCycle) { const handler = ( startTime: number, rawRumEvent: RawRumEvent, @@ -152,8 +141,6 @@ export function trackRumEvents(lifeCycle: LifeCycle, session: RumSession) { trackView(lifeCycle, handler) trackErrors(lifeCycle, handler) - trackRequests(lifeCycle, session, handler) - trackPerformanceTiming(lifeCycle, session, handler) trackCustomUserAction(lifeCycle, handler) trackAutoUserAction(lifeCycle, handler) } @@ -246,96 +233,3 @@ function trackAutoUserAction(lifeCycle: LifeCycle, handler: (startTime: number, }) }) } - -function trackRequests( - lifeCycle: LifeCycle, - session: RumSession, - handler: (startTime: number, event: RumResourceEvent) => void -) { - lifeCycle.subscribe(LifeCycleEventType.REQUEST_COMPLETED, (request: RequestCompleteEvent) => { - if (!session.isTrackedWithResource()) { - return - } - const timing = matchRequestTiming(request) - const kind = request.type === RequestType.XHR ? ResourceType.XHR : ResourceType.FETCH - const startTime = timing ? timing.startTime : request.startTime - const hasBeenTraced = request.traceId && request.spanId - handler(startTime, { - _dd: hasBeenTraced - ? { - spanId: request.spanId!.toDecimalString(), - traceId: request.traceId!.toDecimalString(), - } - : undefined, - date: getTimestamp(startTime), - duration: timing ? computePerformanceResourceDuration(timing) : msToNs(request.duration), - evt: { - category: RumEventCategory.RESOURCE, - }, - http: { - method: request.method, - performance: timing ? computePerformanceResourceDetails(timing) : undefined, - statusCode: request.status, - url: request.url, - }, - network: { - bytesWritten: timing ? computeSize(timing) : undefined, - }, - resource: { - kind, - id: hasBeenTraced ? generateUUID() : undefined, - }, - }) - lifeCycle.notify(LifeCycleEventType.RESOURCE_ADDED_TO_BATCH) - }) -} - -function trackPerformanceTiming( - lifeCycle: LifeCycle, - session: RumSession, - handler: (startTime: number, event: RumResourceEvent | RumLongTaskEvent) => void -) { - lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, (entry) => { - if (entry.entryType === 'resource') { - handleResourceEntry(lifeCycle, session, handler, entry) - } - }) -} - -export function handleResourceEntry( - lifeCycle: LifeCycle, - session: RumSession, - handler: (startTime: number, event: RumResourceEvent) => void, - entry: RumPerformanceResourceTiming -) { - if (!session.isTrackedWithResource()) { - return - } - const resourceKind = computeResourceKind(entry) - if (includes([ResourceType.XHR, ResourceType.FETCH], resourceKind)) { - return - } - handler(entry.startTime, { - _dd: entry.traceId - ? { - traceId: entry.traceId, - } - : undefined, - date: getTimestamp(entry.startTime), - duration: computePerformanceResourceDuration(entry), - evt: { - category: RumEventCategory.RESOURCE, - }, - http: { - performance: computePerformanceResourceDetails(entry), - url: entry.name, - }, - network: { - bytesWritten: computeSize(entry), - }, - resource: { - kind: resourceKind, - }, - }) - lifeCycle.notify(LifeCycleEventType.RESOURCE_ADDED_TO_BATCH) -} diff --git a/packages/rum/src/browser/performanceCollection.ts b/packages/rum/src/browser/performanceCollection.ts index 3631eb502d..4ab80f9594 100644 --- a/packages/rum/src/browser/performanceCollection.ts +++ b/packages/rum/src/browser/performanceCollection.ts @@ -1,8 +1,8 @@ import { Configuration, DOM_EVENT, getRelativeTime, isNumber, monitor } from '@datadog/browser-core' - -import { getDocumentTraceId } from '../domain/getDocumentTraceId' import { LifeCycle, LifeCycleEventType } from '../domain/lifeCycle' -import { FAKE_INITIAL_DOCUMENT, isAllowedRequestUrl } from '../domain/rumEventsCollection/resourceUtils' +import { FAKE_INITIAL_DOCUMENT, isAllowedRequestUrl } from '../domain/rumEventsCollection/resource/resourceUtils' + +import { getDocumentTraceId } from '../domain/tracing/getDocumentTraceId' interface BrowserWindow extends Window { PerformanceObserver?: PerformanceObserver diff --git a/packages/rum/src/domain/assembly.spec.ts b/packages/rum/src/domain/assembly.spec.ts index cb7e77b346..cc5d62b696 100644 --- a/packages/rum/src/domain/assembly.spec.ts +++ b/packages/rum/src/domain/assembly.spec.ts @@ -28,6 +28,7 @@ describe('rum assembly', () => { let lifeCycle: LifeCycle let setGlobalContext: (context: Context) => void let serverRumEvents: ServerRumEvents[] + let isTracked: boolean function generateRawRumEvent( category: RumEventCategory, @@ -45,7 +46,13 @@ describe('rum assembly', () => { } beforeEach(() => { + isTracked = true setupBuilder = setup() + .withSession({ + getId: () => '1234', + isTracked: () => isTracked, + isTrackedWithResource: () => true, + }) .withParentContexts({ findAction: () => ({ userAction: { @@ -167,4 +174,20 @@ describe('rum assembly', () => { }) }) }) + + describe('session', () => { + it('when tracked, it should generate event ', () => { + isTracked = true + + generateRawRumEvent(RumEventCategory.VIEW) + expect(serverRumEvents.length).toBe(1) + }) + + it('when not tracked, it should not generate event ', () => { + isTracked = false + + generateRawRumEvent(RumEventCategory.VIEW) + expect(serverRumEvents.length).toBe(0) + }) + }) }) diff --git a/packages/rum/src/domain/assemblyV2.spec.ts b/packages/rum/src/domain/assemblyV2.spec.ts index 3e33477534..52a5594c16 100644 --- a/packages/rum/src/domain/assemblyV2.spec.ts +++ b/packages/rum/src/domain/assemblyV2.spec.ts @@ -1,5 +1,6 @@ import { Context } from '@datadog/browser-core' import { setup, TestSetupBuilder } from '../../test/specHelper' +import { RumEventCategory } from '../types' import { RawRumEventV2, RumEventType } from '../typesV2' import { LifeCycle, LifeCycleEventType } from './lifeCycle' @@ -33,6 +34,7 @@ describe('rum assembly v2', () => { let lifeCycle: LifeCycle let setGlobalContext: (context: Context) => void let serverRumEvents: ServerRumEvents[] + let isTracked: boolean function generateRawRumEvent( type: RumEventType, @@ -50,7 +52,13 @@ describe('rum assembly v2', () => { } beforeEach(() => { + isTracked = true setupBuilder = setup() + .withSession({ + getId: () => '1234', + isTracked: () => isTracked, + isTrackedWithResource: () => true, + }) .withParentContexts({ findActionV2: () => ({ action: { @@ -174,4 +182,20 @@ describe('rum assembly v2', () => { }) }) }) + + describe('session', () => { + it('when tracked, it should generate event ', () => { + isTracked = true + + generateRawRumEvent(RumEventType.VIEW) + expect(serverRumEvents.length).toBe(1) + }) + + it('when not tracked, it should not generate event ', () => { + isTracked = false + + generateRawRumEvent(RumEventType.VIEW) + expect(serverRumEvents.length).toBe(0) + }) + }) }) diff --git a/packages/rum/src/domain/lifeCycle.ts b/packages/rum/src/domain/lifeCycle.ts index eda292ec75..9dc2b4c29d 100644 --- a/packages/rum/src/domain/lifeCycle.ts +++ b/packages/rum/src/domain/lifeCycle.ts @@ -2,7 +2,7 @@ import { Context, ErrorMessage } from '@datadog/browser-core' import { RumPerformanceEntry } from '../browser/performanceCollection' import { RawRumEvent, RumEvent } from '../types' import { RawRumEventV2, RumEventV2 } from '../typesV2' -import { RequestCompleteEvent, RequestStartEvent } from './rumEventsCollection/requestCollection' +import { RequestCompleteEvent, RequestStartEvent } from './requestCollection' import { AutoActionCreatedEvent, AutoUserAction, CustomUserAction } from './rumEventsCollection/userActionCollection' import { View, ViewCreatedEvent } from './rumEventsCollection/viewCollection' diff --git a/packages/rum/src/domain/rumEventsCollection/requestCollection.spec.ts b/packages/rum/src/domain/requestCollection.spec.ts similarity index 98% rename from packages/rum/src/domain/rumEventsCollection/requestCollection.spec.ts rename to packages/rum/src/domain/requestCollection.spec.ts index d65d575c96..949369ce6a 100644 --- a/packages/rum/src/domain/rumEventsCollection/requestCollection.spec.ts +++ b/packages/rum/src/domain/requestCollection.spec.ts @@ -13,9 +13,9 @@ import { stubXhr, withXhr, } from '@datadog/browser-core' -import { LifeCycle, LifeCycleEventType } from '../lifeCycle' -import { Tracer } from '../tracer' +import { LifeCycle, LifeCycleEventType } from './lifeCycle' import { RequestCompleteEvent, RequestStartEvent, trackFetch, trackXhr } from './requestCollection' +import { Tracer } from './tracing/tracer' const configuration = { ...DEFAULT_CONFIGURATION, diff --git a/packages/rum/src/domain/rumEventsCollection/requestCollection.ts b/packages/rum/src/domain/requestCollection.ts similarity index 94% rename from packages/rum/src/domain/rumEventsCollection/requestCollection.ts rename to packages/rum/src/domain/requestCollection.ts index 01bb5a08ce..3058342940 100644 --- a/packages/rum/src/domain/rumEventsCollection/requestCollection.ts +++ b/packages/rum/src/domain/requestCollection.ts @@ -9,9 +9,9 @@ import { XhrCompleteContext, XhrStartContext, } from '@datadog/browser-core' -import { LifeCycle, LifeCycleEventType } from '../lifeCycle' -import { startTracer, TraceIdentifier, Tracer } from '../tracer' -import { isAllowedRequestUrl } from './resourceUtils' +import { LifeCycle, LifeCycleEventType } from './lifeCycle' +import { isAllowedRequestUrl } from './rumEventsCollection/resource/resourceUtils' +import { startTracer, TraceIdentifier, Tracer } from './tracing/tracer' export interface RequestStartEvent { requestIndex: number diff --git a/packages/rum/src/domain/rumEventsCollection/longTaskCollection.spec.ts b/packages/rum/src/domain/rumEventsCollection/longTask/longTaskCollection.spec.ts similarity index 90% rename from packages/rum/src/domain/rumEventsCollection/longTaskCollection.spec.ts rename to packages/rum/src/domain/rumEventsCollection/longTask/longTaskCollection.spec.ts index 09e5011de8..355d7c35e8 100644 --- a/packages/rum/src/domain/rumEventsCollection/longTaskCollection.spec.ts +++ b/packages/rum/src/domain/rumEventsCollection/longTask/longTaskCollection.spec.ts @@ -1,8 +1,8 @@ -import { setup, TestSetupBuilder } from '../../../test/specHelper' -import { RumPerformanceEntry } from '../../browser/performanceCollection' -import { RumEventCategory } from '../../index' -import { RumEventType } from '../../typesV2' -import { LifeCycleEventType } from '../lifeCycle' +import { setup, TestSetupBuilder } from '../../../../test/specHelper' +import { RumPerformanceEntry } from '../../../browser/performanceCollection' +import { RumEventCategory } from '../../../index' +import { RumEventType } from '../../../typesV2' +import { LifeCycleEventType } from '../../lifeCycle' import { startLongTaskCollection } from './longTaskCollection' describe('long task collection', () => { diff --git a/packages/rum/src/domain/rumEventsCollection/longTaskCollection.ts b/packages/rum/src/domain/rumEventsCollection/longTask/longTaskCollection.ts similarity index 84% rename from packages/rum/src/domain/rumEventsCollection/longTaskCollection.ts rename to packages/rum/src/domain/rumEventsCollection/longTask/longTaskCollection.ts index 73f6fba227..402c279566 100644 --- a/packages/rum/src/domain/rumEventsCollection/longTaskCollection.ts +++ b/packages/rum/src/domain/rumEventsCollection/longTask/longTaskCollection.ts @@ -1,7 +1,7 @@ import { Configuration, getTimestamp, msToNs } from '@datadog/browser-core' -import { RumEventCategory, RumLongTaskEvent } from '../../types' -import { RumEventType, RumLongTaskEventV2 } from '../../typesV2' -import { LifeCycle, LifeCycleEventType } from '../lifeCycle' +import { RumEventCategory, RumLongTaskEvent } from '../../../types' +import { RumEventType, RumLongTaskEventV2 } from '../../../typesV2' +import { LifeCycle, LifeCycleEventType } from '../../lifeCycle' export function startLongTaskCollection(lifeCycle: LifeCycle, configuration: Configuration) { lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, (entry) => { diff --git a/packages/rum/src/domain/rumEventsCollection/matchRequestTiming.spec.ts b/packages/rum/src/domain/rumEventsCollection/resource/matchRequestTiming.spec.ts similarity index 95% rename from packages/rum/src/domain/rumEventsCollection/matchRequestTiming.spec.ts rename to packages/rum/src/domain/rumEventsCollection/resource/matchRequestTiming.spec.ts index 61a5baeaee..a3157032f2 100644 --- a/packages/rum/src/domain/rumEventsCollection/matchRequestTiming.spec.ts +++ b/packages/rum/src/domain/rumEventsCollection/resource/matchRequestTiming.spec.ts @@ -1,8 +1,8 @@ import { isIE } from '@datadog/browser-core' -import { RumPerformanceResourceTiming } from '../../browser/performanceCollection' +import { RumPerformanceResourceTiming } from '../../../browser/performanceCollection' +import { RequestCompleteEvent } from '../../requestCollection' import { matchRequestTiming } from './matchRequestTiming' -import { RequestCompleteEvent } from './requestCollection' describe('matchRequestTiming', () => { const FAKE_REQUEST: Partial = { startTime: 100, duration: 500 } diff --git a/packages/rum/src/domain/rumEventsCollection/matchRequestTiming.ts b/packages/rum/src/domain/rumEventsCollection/resource/matchRequestTiming.ts similarity index 90% rename from packages/rum/src/domain/rumEventsCollection/matchRequestTiming.ts rename to packages/rum/src/domain/rumEventsCollection/resource/matchRequestTiming.ts index 4d5da2ba37..8e0cac7c34 100644 --- a/packages/rum/src/domain/rumEventsCollection/matchRequestTiming.ts +++ b/packages/rum/src/domain/rumEventsCollection/resource/matchRequestTiming.ts @@ -1,5 +1,5 @@ -import { RumPerformanceResourceTiming } from '../../browser/performanceCollection' -import { RequestCompleteEvent } from './requestCollection' +import { RumPerformanceResourceTiming } from '../../../browser/performanceCollection' +import { RequestCompleteEvent } from '../../requestCollection' interface Timing { startTime: number diff --git a/packages/rum/src/domain/rumEventsCollection/resource/resourceCollection.spec.ts b/packages/rum/src/domain/rumEventsCollection/resource/resourceCollection.spec.ts new file mode 100644 index 0000000000..4d40322c9c --- /dev/null +++ b/packages/rum/src/domain/rumEventsCollection/resource/resourceCollection.spec.ts @@ -0,0 +1,452 @@ +import { RequestType, ResourceType } from '@datadog/browser-core' +import { setup, TestSetupBuilder } from '../../../../test/specHelper' +import { RumPerformanceResourceTiming } from '../../../browser/performanceCollection' +import { RumEventCategory, RumResourceEvent } from '../../../types' +import { RumEventType, RumResourceEventV2 } from '../../../typesV2' +import { LifeCycleEventType } from '../../lifeCycle' +import { RequestCompleteEvent } from '../../requestCollection' +import { RumSession } from '../../rumSession' +import { TraceIdentifier } from '../../tracing/tracer' +import { startResourceCollection } from './resourceCollection' + +describe('resourceCollection', () => { + let setupBuilder: TestSetupBuilder + + describe('when resource tracking is enabled', () => { + beforeEach(() => { + setupBuilder = setup() + .withSession({ + getId: () => '1234', + isTracked: () => true, + isTrackedWithResource: () => true, + }) + .beforeBuild((lifeCycle, configuration, session: RumSession) => { + configuration.isEnabled = () => false + startResourceCollection(lifeCycle, configuration, session) + }) + }) + + afterEach(() => { + setupBuilder.cleanup() + }) + + it('should create resource from performance entry', () => { + const { lifeCycle, rawRumEvents } = setupBuilder.build() + lifeCycle.notify( + LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, + createResourceEntry({ + duration: 100, + name: 'https://resource.com/valid', + startTime: 1234, + }) + ) + + expect(rawRumEvents[0].startTime).toBe(1234) + expect(rawRumEvents[0].rawRumEvent).toEqual({ + date: (jasmine.any(Number) as unknown) as number, + duration: 100 * 1e6, + evt: { + category: RumEventCategory.RESOURCE, + }, + http: { + performance: jasmine.anything() as any, + url: 'https://resource.com/valid', + }, + network: { + bytesWritten: undefined, + }, + resource: { + kind: ResourceType.OTHER, + }, + }) + }) + + it('should create resource from completed request', () => { + const { lifeCycle, rawRumEvents } = setupBuilder.build() + lifeCycle.notify( + LifeCycleEventType.REQUEST_COMPLETED, + createCompletedRequest({ + duration: 100, + method: 'GET', + startTime: 1234, + status: 200, + type: RequestType.XHR, + url: 'https://resource.com/valid', + }) + ) + + expect(rawRumEvents[0].startTime).toBe(1234) + expect(rawRumEvents[0].rawRumEvent).toEqual({ + date: (jasmine.any(Number) as unknown) as number, + duration: 100 * 1e6, + evt: { + category: RumEventCategory.RESOURCE, + }, + http: { + method: 'GET', + statusCode: 200, + url: 'https://resource.com/valid', + }, + resource: { + kind: ResourceType.XHR, + }, + }) + }) + }) + + describe('when resource tracking is disabled', () => { + beforeEach(() => { + setupBuilder = setup() + .withSession({ + getId: () => '1234', + isTracked: () => true, + isTrackedWithResource: () => false, + }) + .beforeBuild((lifeCycle, configuration, session: RumSession) => { + configuration.isEnabled = () => false + startResourceCollection(lifeCycle, configuration, session) + }) + }) + + afterEach(() => { + setupBuilder.cleanup() + }) + + it('should not create resource from performance entry', () => { + const { lifeCycle, rawRumEvents } = setupBuilder.build() + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, createResourceEntry()) + + expect(rawRumEvents.length).toBe(0) + }) + + it('should not create resource from completed request', () => { + const { lifeCycle, rawRumEvents } = setupBuilder.build() + lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, createCompletedRequest()) + + expect(rawRumEvents.length).toBe(0) + }) + }) + + describe('when resource tracking change', () => { + let isTrackedWithResource = true + + beforeEach(() => { + setupBuilder = setup() + .withSession({ + getId: () => '1234', + isTracked: () => true, + isTrackedWithResource: () => isTrackedWithResource, + }) + .beforeBuild((lifeCycle, configuration, session: RumSession) => { + configuration.isEnabled = () => false + startResourceCollection(lifeCycle, configuration, session) + }) + }) + + afterEach(() => { + setupBuilder.cleanup() + }) + + it('should enable/disable resource creation from performance entry', () => { + const { lifeCycle, rawRumEvents } = setupBuilder.build() + + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, createResourceEntry()) + expect(rawRumEvents.length).toBe(1) + + isTrackedWithResource = false + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, createResourceEntry()) + expect(rawRumEvents.length).toBe(1) + + isTrackedWithResource = true + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, createResourceEntry()) + expect(rawRumEvents.length).toBe(2) + }) + + it('should enable/disable resource creation from completed request', () => { + const { lifeCycle, rawRumEvents } = setupBuilder.build() + + lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, createCompletedRequest()) + expect(rawRumEvents.length).toBe(1) + + isTrackedWithResource = false + lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, createCompletedRequest()) + expect(rawRumEvents.length).toBe(1) + + isTrackedWithResource = true + lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, createCompletedRequest()) + expect(rawRumEvents.length).toBe(2) + }) + }) + + describe('tracing info', () => { + beforeEach(() => { + setupBuilder = setup().beforeBuild((lifeCycle, configuration, session: RumSession) => { + configuration.isEnabled = () => false + startResourceCollection(lifeCycle, configuration, session) + }) + }) + + afterEach(() => { + setupBuilder.cleanup() + }) + + it('should be processed from traced initial document', () => { + const { lifeCycle, rawRumEvents } = setupBuilder.build() + lifeCycle.notify( + LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, + createResourceEntry({ + traceId: 'xxx', + }) + ) + + const traceInfo = (rawRumEvents[0].rawRumEvent as RumResourceEvent)._dd! + expect(traceInfo).toBeDefined() + expect(traceInfo.traceId).toBe('xxx') + }) + + it('should be processed from completed request', () => { + const { lifeCycle, rawRumEvents } = setupBuilder.build() + lifeCycle.notify( + LifeCycleEventType.REQUEST_COMPLETED, + createCompletedRequest({ + spanId: new TraceIdentifier(), + traceId: new TraceIdentifier(), + }) + ) + + const traceInfo = (rawRumEvents[0].rawRumEvent as RumResourceEvent)._dd! + expect(traceInfo).toBeDefined() + expect(traceInfo.traceId).toBeDefined() + expect(traceInfo.spanId).toBeDefined() + }) + }) +}) + +describe('resourceCollection V2', () => { + let setupBuilder: TestSetupBuilder + + describe('when resource tracking is enabled', () => { + beforeEach(() => { + setupBuilder = setup() + .withSession({ + getId: () => '1234', + isTracked: () => true, + isTrackedWithResource: () => true, + }) + .beforeBuild((lifeCycle, configuration, session: RumSession) => { + configuration.isEnabled = () => true + startResourceCollection(lifeCycle, configuration, session) + }) + }) + + afterEach(() => { + setupBuilder.cleanup() + }) + + it('should create resource from performance entry', () => { + const { lifeCycle, rawRumEventsV2 } = setupBuilder.build() + lifeCycle.notify( + LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, + createResourceEntry({ + duration: 100, + name: 'https://resource.com/valid', + startTime: 1234, + }) + ) + + expect(rawRumEventsV2[0].startTime).toBe(1234) + expect(rawRumEventsV2[0].rawRumEvent).toEqual({ + date: (jasmine.any(Number) as unknown) as number, + resource: { + download: jasmine.anything(), + duration: 100 * 1e6, + firstByte: jasmine.anything(), + redirect: jasmine.anything(), + size: undefined, + type: ResourceType.OTHER, + url: 'https://resource.com/valid', + }, + type: RumEventType.RESOURCE, + }) + }) + + it('should create resource from completed request', () => { + const { lifeCycle, rawRumEventsV2 } = setupBuilder.build() + lifeCycle.notify( + LifeCycleEventType.REQUEST_COMPLETED, + createCompletedRequest({ + duration: 100, + method: 'GET', + startTime: 1234, + status: 200, + type: RequestType.XHR, + url: 'https://resource.com/valid', + }) + ) + + expect(rawRumEventsV2[0].startTime).toBe(1234) + expect(rawRumEventsV2[0].rawRumEvent).toEqual({ + date: (jasmine.any(Number) as unknown) as number, + resource: { + duration: 100 * 1e6, + method: 'GET', + statusCode: 200, + type: ResourceType.XHR, + url: 'https://resource.com/valid', + }, + type: RumEventType.RESOURCE, + }) + }) + }) + + describe('when resource tracking is disabled', () => { + beforeEach(() => { + setupBuilder = setup() + .withSession({ + getId: () => '1234', + isTracked: () => true, + isTrackedWithResource: () => false, + }) + .beforeBuild((lifeCycle, configuration, session: RumSession) => { + configuration.isEnabled = () => true + startResourceCollection(lifeCycle, configuration, session) + }) + }) + + afterEach(() => { + setupBuilder.cleanup() + }) + + it('should not create resource from performance entry', () => { + const { lifeCycle, rawRumEventsV2 } = setupBuilder.build() + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, createResourceEntry()) + + expect(rawRumEventsV2.length).toBe(0) + }) + + it('should not create resource from completed request', () => { + const { lifeCycle, rawRumEventsV2 } = setupBuilder.build() + lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, createCompletedRequest()) + + expect(rawRumEventsV2.length).toBe(0) + }) + }) + + describe('when resource tracking change', () => { + let isTrackedWithResource = true + + beforeEach(() => { + setupBuilder = setup() + .withSession({ + getId: () => '1234', + isTracked: () => true, + isTrackedWithResource: () => isTrackedWithResource, + }) + .beforeBuild((lifeCycle, configuration, session: RumSession) => { + configuration.isEnabled = () => true + startResourceCollection(lifeCycle, configuration, session) + }) + }) + + afterEach(() => { + setupBuilder.cleanup() + }) + + it('should enable/disable resource creation from performance entry', () => { + const { lifeCycle, rawRumEventsV2 } = setupBuilder.build() + + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, createResourceEntry()) + expect(rawRumEventsV2.length).toBe(1) + + isTrackedWithResource = false + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, createResourceEntry()) + expect(rawRumEventsV2.length).toBe(1) + + isTrackedWithResource = true + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, createResourceEntry()) + expect(rawRumEventsV2.length).toBe(2) + }) + + it('should enable/disable resource creation from completed request', () => { + const { lifeCycle, rawRumEventsV2 } = setupBuilder.build() + + lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, createCompletedRequest()) + expect(rawRumEventsV2.length).toBe(1) + + isTrackedWithResource = false + lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, createCompletedRequest()) + expect(rawRumEventsV2.length).toBe(1) + + isTrackedWithResource = true + lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, createCompletedRequest()) + expect(rawRumEventsV2.length).toBe(2) + }) + }) + + describe('tracing info', () => { + beforeEach(() => { + setupBuilder = setup().beforeBuild((lifeCycle, configuration, session: RumSession) => { + configuration.isEnabled = () => true + startResourceCollection(lifeCycle, configuration, session) + }) + }) + + afterEach(() => { + setupBuilder.cleanup() + }) + + it('should be processed from traced initial document', () => { + const { lifeCycle, rawRumEventsV2 } = setupBuilder.build() + lifeCycle.notify( + LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, + createResourceEntry({ + traceId: 'xxx', + }) + ) + + const traceInfo = (rawRumEventsV2[0].rawRumEvent as RumResourceEventV2)._dd! + expect(traceInfo).toBeDefined() + expect(traceInfo.traceId).toBe('xxx') + }) + + it('should be processed from completed request', () => { + const { lifeCycle, rawRumEventsV2 } = setupBuilder.build() + lifeCycle.notify( + LifeCycleEventType.REQUEST_COMPLETED, + createCompletedRequest({ + spanId: new TraceIdentifier(), + traceId: new TraceIdentifier(), + }) + ) + + const traceInfo = (rawRumEventsV2[0].rawRumEvent as RumResourceEventV2)._dd! + expect(traceInfo).toBeDefined() + expect(traceInfo.traceId).toBeDefined() + expect(traceInfo.spanId).toBeDefined() + }) + }) +}) + +function createResourceEntry(details?: Partial): RumPerformanceResourceTiming { + const entry: Partial = { + duration: 100, + entryType: 'resource', + name: 'https://resource.com/valid', + startTime: 1234, + ...details, + } + return entry as RumPerformanceResourceTiming +} + +function createCompletedRequest(details?: Partial): RequestCompleteEvent { + const request: Partial = { + duration: 100, + method: 'GET', + startTime: 1234, + status: 200, + type: RequestType.XHR, + url: 'https://resource.com/valid', + ...details, + } + return request as RequestCompleteEvent +} diff --git a/packages/rum/src/domain/rumEventsCollection/resource/resourceCollection.ts b/packages/rum/src/domain/rumEventsCollection/resource/resourceCollection.ts new file mode 100644 index 0000000000..cf8bdaa042 --- /dev/null +++ b/packages/rum/src/domain/rumEventsCollection/resource/resourceCollection.ts @@ -0,0 +1,185 @@ +import { + combine, + Configuration, + generateUUID, + getTimestamp, + msToNs, + RequestType, + ResourceType, +} from '@datadog/browser-core' +import { RumPerformanceResourceTiming } from '../../../browser/performanceCollection' +import { RumEventCategory, RumResourceEvent } from '../../../types' +import { RumEventType, RumResourceEventV2 } from '../../../typesV2' +import { LifeCycle, LifeCycleEventType } from '../../lifeCycle' +import { RequestCompleteEvent } from '../../requestCollection' +import { RumSession } from '../../rumSession' +import { matchRequestTiming } from './matchRequestTiming' +import { + computePerformanceResourceDetails, + computePerformanceResourceDuration, + computeResourceKind, + computeSize, + isRequestKind, +} from './resourceUtils' + +export function startResourceCollection(lifeCycle: LifeCycle, configuration: Configuration, session: RumSession) { + lifeCycle.subscribe(LifeCycleEventType.REQUEST_COMPLETED, (request: RequestCompleteEvent) => { + if (session.isTrackedWithResource()) { + configuration.isEnabled('v2_format') + ? lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_V2_COLLECTED, processRequestV2(request)) + : lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processRequest(request)) + lifeCycle.notify(LifeCycleEventType.RESOURCE_ADDED_TO_BATCH) + } + }) + + lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, (entry) => { + if (session.isTrackedWithResource() && entry.entryType === 'resource' && !isRequestKind(entry)) { + configuration.isEnabled('v2_format') + ? lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_V2_COLLECTED, processResourceEntryV2(entry)) + : lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processResourceEntry(entry)) + lifeCycle.notify(LifeCycleEventType.RESOURCE_ADDED_TO_BATCH) + } + }) +} + +function processRequest(request: RequestCompleteEvent) { + const kind = request.type === RequestType.XHR ? ResourceType.XHR : ResourceType.FETCH + + const matchingTiming = matchRequestTiming(request) + const startTime = matchingTiming ? matchingTiming.startTime : request.startTime + const correspondingTimingOverrides = matchingTiming ? computePerformanceEntryMetrics(matchingTiming) : undefined + + const tracingInfo = computeRequestTracingInfo(request) + + const resourceEvent: RumResourceEvent = combine( + { + date: getTimestamp(startTime), + duration: msToNs(request.duration), + evt: { + category: RumEventCategory.RESOURCE as const, + }, + http: { + method: request.method, + statusCode: request.status, + url: request.url, + }, + resource: { + kind, + }, + }, + tracingInfo, + correspondingTimingOverrides + ) + return { startTime, rawRumEvent: resourceEvent } +} + +function processRequestV2(request: RequestCompleteEvent) { + const type = request.type === RequestType.XHR ? ResourceType.XHR : ResourceType.FETCH + + const matchingTiming = matchRequestTiming(request) + const startTime = matchingTiming ? matchingTiming.startTime : request.startTime + const correspondingTimingOverrides = matchingTiming ? computePerformanceEntryMetricsV2(matchingTiming) : undefined + + const tracingInfo = computeRequestTracingInfo(request) + + const resourceEvent = combine( + { + date: getTimestamp(startTime), + resource: { + type, + duration: msToNs(request.duration), + method: request.method, + statusCode: request.status, + url: request.url, + }, + type: RumEventType.RESOURCE, + }, + tracingInfo, + correspondingTimingOverrides + ) + return { startTime, rawRumEvent: resourceEvent as RumResourceEventV2 } +} + +function processResourceEntry(entry: RumPerformanceResourceTiming) { + const resourceKind = computeResourceKind(entry) + const entryMetrics = computePerformanceEntryMetrics(entry) + const tracingInfo = computeEntryTracingInfo(entry) + + const resourceEvent: RumResourceEvent = combine( + { + date: getTimestamp(entry.startTime), + evt: { + category: RumEventCategory.RESOURCE as const, + }, + http: { + url: entry.name, + }, + resource: { + kind: resourceKind, + }, + }, + tracingInfo, + entryMetrics + ) + return { startTime: entry.startTime, rawRumEvent: resourceEvent } +} + +function processResourceEntryV2(entry: RumPerformanceResourceTiming) { + const type = computeResourceKind(entry) + const entryMetrics = computePerformanceEntryMetricsV2(entry) + const tracingInfo = computeEntryTracingInfo(entry) + + const resourceEvent = combine( + { + date: getTimestamp(entry.startTime), + resource: { + type, + url: entry.name, + }, + type: RumEventType.RESOURCE, + }, + tracingInfo, + entryMetrics + ) + return { startTime: entry.startTime, rawRumEvent: resourceEvent as RumResourceEventV2 } +} + +function computePerformanceEntryMetrics(timing: RumPerformanceResourceTiming) { + return { + duration: computePerformanceResourceDuration(timing), + http: { + performance: computePerformanceResourceDetails(timing), + }, + network: { + bytesWritten: computeSize(timing), + }, + } +} + +function computePerformanceEntryMetricsV2(timing: RumPerformanceResourceTiming) { + return { + resource: { + duration: computePerformanceResourceDuration(timing), + size: computeSize(timing), + ...computePerformanceResourceDetails(timing), + }, + } +} + +function computeRequestTracingInfo(request: RequestCompleteEvent) { + const hasBeenTraced = request.traceId && request.spanId + if (!hasBeenTraced) { + return undefined + } + return { + _dd: { + spanId: request.spanId!.toDecimalString(), + traceId: request.traceId!.toDecimalString(), + }, + resource: { id: generateUUID() }, + } +} + +function computeEntryTracingInfo(entry: RumPerformanceResourceTiming) { + return entry.traceId ? { _dd: { traceId: entry.traceId } } : undefined +} diff --git a/packages/rum/src/domain/rumEventsCollection/resourceUtils.spec.ts b/packages/rum/src/domain/rumEventsCollection/resource/resourceUtils.spec.ts similarity index 86% rename from packages/rum/src/domain/rumEventsCollection/resourceUtils.spec.ts rename to packages/rum/src/domain/rumEventsCollection/resource/resourceUtils.spec.ts index 87d288ed57..6cd8a9b0ee 100644 --- a/packages/rum/src/domain/rumEventsCollection/resourceUtils.spec.ts +++ b/packages/rum/src/domain/rumEventsCollection/resource/resourceUtils.spec.ts @@ -1,8 +1,9 @@ import { Configuration, DEFAULT_CONFIGURATION, SPEC_ENDPOINTS } from '@datadog/browser-core' -import { RumPerformanceResourceTiming } from '../../browser/performanceCollection' +import { RumPerformanceResourceTiming } from '../../../browser/performanceCollection' import { computePerformanceResourceDetails, computePerformanceResourceDuration, + computeResourceKind, isAllowedRequestUrl, } from './resourceUtils' @@ -28,6 +29,49 @@ function generateResourceWith(overrides: Partial) return completeTiming as RumPerformanceResourceTiming } +describe('computeResourceKind', () => { + ;[ + { + description: 'file extension with query params', + expected: 'js', + name: 'http://localhost/test.js?from=foo.css', + }, + { + description: 'css extension', + expected: 'css', + name: 'http://localhost/test.css', + }, + { + description: 'image initiator', + expected: 'image', + initiatorType: 'img', + name: 'http://localhost/test', + }, + { + description: 'image extension', + expected: 'image', + name: 'http://localhost/test.jpg', + }, + ].forEach( + ({ + description, + name, + initiatorType, + expected, + }: { + description: string + name: string + initiatorType?: string + expected: string + }) => { + it(`should compute resource kind: ${description}`, () => { + const entry = generateResourceWith({ initiatorType, name }) + expect(computeResourceKind(entry)).toEqual(expected) + }) + } + ) +}) + describe('computePerformanceResourceDetails', () => { it('should not compute entry without detailed timings', () => { expect( @@ -178,6 +222,12 @@ describe('computePerformanceResourceDetails', () => { reason: 'secureConnectionStart > connectEnd', secureConnectionStart: 20, }, + { + connectEnd: 10, + connectStart: -3, + fetchStart: 10, + reason: 'negative timing start', + }, ].forEach(({ reason, ...overrides }) => { it(`should not compute entry when ${reason}`, () => { expect(computePerformanceResourceDetails(generateResourceWith(overrides))).toBeUndefined() diff --git a/packages/rum/src/domain/rumEventsCollection/resourceUtils.ts b/packages/rum/src/domain/rumEventsCollection/resource/resourceUtils.ts similarity index 95% rename from packages/rum/src/domain/rumEventsCollection/resourceUtils.ts rename to packages/rum/src/domain/rumEventsCollection/resource/resourceUtils.ts index 543072c58c..cea2079c42 100644 --- a/packages/rum/src/domain/rumEventsCollection/resourceUtils.ts +++ b/packages/rum/src/domain/rumEventsCollection/resource/resourceUtils.ts @@ -9,7 +9,7 @@ import { ResourceType, } from '@datadog/browser-core' -import { RumPerformanceResourceTiming } from '../../browser/performanceCollection' +import { RumPerformanceResourceTiming } from '../../../browser/performanceCollection' export interface PerformanceResourceDetailsElement { duration: number @@ -71,6 +71,10 @@ function areInOrder(...numbers: number[]) { return true } +export function isRequestKind(timing: RumPerformanceResourceTiming) { + return timing.initiatorType === 'xmlhttprequest' || timing.initiatorType === 'fetch' +} + export function computePerformanceResourceDuration(entry: RumPerformanceResourceTiming): number { const { duration, startTime, responseEnd } = entry diff --git a/packages/rum/src/domain/getDocumentTraceId.spec.ts b/packages/rum/src/domain/tracing/getDocumentTraceId.spec.ts similarity index 100% rename from packages/rum/src/domain/getDocumentTraceId.spec.ts rename to packages/rum/src/domain/tracing/getDocumentTraceId.spec.ts diff --git a/packages/rum/src/domain/getDocumentTraceId.ts b/packages/rum/src/domain/tracing/getDocumentTraceId.ts similarity index 100% rename from packages/rum/src/domain/getDocumentTraceId.ts rename to packages/rum/src/domain/tracing/getDocumentTraceId.ts diff --git a/packages/rum/src/domain/tracer.spec.ts b/packages/rum/src/domain/tracing/tracer.spec.ts similarity index 99% rename from packages/rum/src/domain/tracer.spec.ts rename to packages/rum/src/domain/tracing/tracer.spec.ts index c3d6733024..517de7aa78 100644 --- a/packages/rum/src/domain/tracer.spec.ts +++ b/packages/rum/src/domain/tracing/tracer.spec.ts @@ -6,7 +6,7 @@ import { objectEntries, XhrCompleteContext, } from '@datadog/browser-core' -import { setup, TestSetupBuilder } from '../../test/specHelper' +import { setup, TestSetupBuilder } from '../../../test/specHelper' import { startTracer, TraceIdentifier } from './tracer' describe('tracer', () => { diff --git a/packages/rum/src/domain/tracer.ts b/packages/rum/src/domain/tracing/tracer.ts similarity index 100% rename from packages/rum/src/domain/tracer.ts rename to packages/rum/src/domain/tracing/tracer.ts diff --git a/packages/rum/src/domain/trackPageActivities.spec.ts b/packages/rum/src/domain/trackPageActivities.spec.ts index 2c59bb1ea0..decdf3a85c 100644 --- a/packages/rum/src/domain/trackPageActivities.spec.ts +++ b/packages/rum/src/domain/trackPageActivities.spec.ts @@ -1,7 +1,7 @@ import { noop, Observable } from '@datadog/browser-core' import { RumPerformanceNavigationTiming, RumPerformanceResourceTiming } from '../browser/performanceCollection' import { LifeCycle, LifeCycleEventType } from './lifeCycle' -import { RequestCompleteEvent } from './rumEventsCollection/requestCollection' +import { RequestCompleteEvent } from './requestCollection' import { PAGE_ACTIVITY_END_DELAY, PAGE_ACTIVITY_MAX_DURATION, diff --git a/packages/rum/src/types.ts b/packages/rum/src/types.ts index 9c10cde367..4923feca70 100644 --- a/packages/rum/src/types.ts +++ b/packages/rum/src/types.ts @@ -1,5 +1,5 @@ import { Context, ErrorContext, HttpContext, ResourceType } from '@datadog/browser-core' -import { PerformanceResourceDetails } from './domain/rumEventsCollection/resourceUtils' +import { PerformanceResourceDetails } from './domain/rumEventsCollection/resource/resourceUtils' import { ActionType, UserActionMeasures } from './domain/rumEventsCollection/userActionCollection' import { ViewLoadingType, ViewMeasures } from './domain/rumEventsCollection/viewCollection' @@ -23,7 +23,7 @@ export interface RumResourceEvent { statusCode?: number url: string } - network: { + network?: { bytesWritten?: number } resource: { diff --git a/packages/rum/src/typesV2.ts b/packages/rum/src/typesV2.ts index d5b4a76099..f6f51a86ca 100644 --- a/packages/rum/src/typesV2.ts +++ b/packages/rum/src/typesV2.ts @@ -1,5 +1,5 @@ import { Context, ErrorSource, HttpContext, ResourceType } from '@datadog/browser-core' -import { PerformanceResourceDetailsElement } from './domain/rumEventsCollection/resourceUtils' +import { PerformanceResourceDetailsElement } from './domain/rumEventsCollection/resource/resourceUtils' import { ActionType } from './domain/rumEventsCollection/userActionCollection' import { ViewLoadingType } from './domain/rumEventsCollection/viewCollection' @@ -26,8 +26,8 @@ export interface RumResourceEventV2 { dns?: PerformanceResourceDetailsElement connect?: PerformanceResourceDetailsElement ssl?: PerformanceResourceDetailsElement - firstByte: PerformanceResourceDetailsElement - download: PerformanceResourceDetailsElement + firstByte?: PerformanceResourceDetailsElement + download?: PerformanceResourceDetailsElement } _dd?: { traceId: string diff --git a/packages/rum/test/specHelper.ts b/packages/rum/test/specHelper.ts index 5498fdbe12..24a9641034 100644 --- a/packages/rum/test/specHelper.ts +++ b/packages/rum/test/specHelper.ts @@ -40,7 +40,9 @@ export interface TestSetupBuilder { withFakeClock: () => TestSetupBuilder withFakeServer: () => TestSetupBuilder withPerformanceObserverStubBuilder: () => TestSetupBuilder - beforeBuild: (callback: (lifeCycle: LifeCycle, configuration: Configuration) => void) => TestSetupBuilder + beforeBuild: ( + callback: (lifeCycle: LifeCycle, configuration: Configuration, session: RumSession) => void + ) => TestSetupBuilder cleanup: () => void build: () => TestIO @@ -77,7 +79,7 @@ export function setup(): TestSetupBuilder { } const lifeCycle = new LifeCycle() const cleanupTasks: Array<() => void> = [] - const beforeBuildTasks: Array<(lifeCycle: LifeCycle, configuration: Configuration) => void> = [] + const beforeBuildTasks: Array<(lifeCycle: LifeCycle, configuration: Configuration, session: RumSession) => void> = [] const buildTasks: Array<() => void> = [] const rawRumEvents: Array<{ startTime: number @@ -227,12 +229,12 @@ export function setup(): TestSetupBuilder { cleanupTasks.push(() => (browserWindow.PerformanceObserver = original)) return setupBuilder }, - beforeBuild(callback: (lifeCycle: LifeCycle, configuration: Configuration) => void) { + beforeBuild(callback: (lifeCycle: LifeCycle, configuration: Configuration, session: RumSession) => void) { beforeBuildTasks.push(callback) return setupBuilder }, build() { - beforeBuildTasks.forEach((task) => task(lifeCycle, configuration as Configuration)) + beforeBuildTasks.forEach((task) => task(lifeCycle, configuration as Configuration, session)) buildTasks.forEach((task) => task()) lifeCycle.subscribe(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, (data) => rawRumEvents.push(data)) lifeCycle.subscribe(LifeCycleEventType.RAW_RUM_EVENT_V2_COLLECTED, (data) => {