Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ [RUMF-1467] Collect user data telemetry #1941

Merged
merged 19 commits into from
Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/src/domain/telemetry/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const enum TelemetryService {
export interface Telemetry {
setContextProvider: (provider: () => Context) => void
observable: Observable<TelemetryEvent & Context>
enabled: boolean
}

const TELEMETRY_EXCLUDED_SITES: string[] = [INTAKE_SITE_US1_FED]
Expand Down Expand Up @@ -90,6 +91,7 @@ export function startTelemetry(telemetryService: TelemetryService, configuration
contextProvider = provider
},
observable,
enabled: telemetryConfiguration.telemetryEnabled,
amortemousque marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
6 changes: 3 additions & 3 deletions packages/rum-core/src/boot/rumPublicApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ describe('rum public api', () => {

rumPublicApi.init(MANUAL_CONFIGURATION)
expect(startRumSpy).toHaveBeenCalled()
expect(startRumSpy.calls.argsFor(0)[4]).toEqual({ name: 'foo' })
expect(startRumSpy.calls.argsFor(0)[6]).toEqual({ name: 'foo' })
expect(recorderApiOnRumStartSpy).toHaveBeenCalled()
expect(startViewSpy).not.toHaveBeenCalled()
})
Expand All @@ -761,7 +761,7 @@ describe('rum public api', () => {

rumPublicApi.startView('foo')
expect(startRumSpy).toHaveBeenCalled()
expect(startRumSpy.calls.argsFor(0)[4]).toEqual({ name: 'foo' })
expect(startRumSpy.calls.argsFor(0)[6]).toEqual({ name: 'foo' })
expect(recorderApiOnRumStartSpy).toHaveBeenCalled()
expect(startViewSpy).not.toHaveBeenCalled()
})
Expand All @@ -772,7 +772,7 @@ describe('rum public api', () => {
rumPublicApi.startView('bar')

expect(startRumSpy).toHaveBeenCalled()
expect(startRumSpy.calls.argsFor(0)[4]).toEqual({ name: 'foo' })
expect(startRumSpy.calls.argsFor(0)[6]).toEqual({ name: 'foo' })
expect(recorderApiOnRumStartSpy).toHaveBeenCalled()
expect(startViewSpy).toHaveBeenCalled()
expect(startViewSpy.calls.argsFor(0)[0]).toEqual({ name: 'bar' })
Expand Down
2 changes: 2 additions & 0 deletions packages/rum-core/src/boot/rumPublicApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ export function makeRumPublicApi(
hasReplay: recorderApi.isRecording() ? true : undefined,
}),
recorderApi,
globalContextManager,
userContextManager,
bcaudan marked this conversation as resolved.
Show resolved Hide resolved
initialViewOptions
)

Expand Down
10 changes: 9 additions & 1 deletion packages/rum-core/src/boot/startRum.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { RelativeTime, Observable, RawError, Duration } from '@datadog/browser-core'
import {
createContextManager,
stopSessionManager,
toServerDuration,
ONE_SECOND,
Expand Down Expand Up @@ -301,7 +302,14 @@ describe('view events', () => {

beforeEach(() => {
setupBuilder = setup().beforeBuild(({ configuration }) => {
startRum({} as RumInitConfiguration, configuration, () => ({ context: {}, user: {} }), noopRecorderApi)
startRum(
{} as RumInitConfiguration,
configuration,
() => ({ context: {}, user: {} }),
noopRecorderApi,
createContextManager(),
createContextManager()
)
})
interceptor = interceptRequests()
})
Expand Down
20 changes: 18 additions & 2 deletions packages/rum-core/src/boot/startRum.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Observable, TelemetryEvent, RawError } from '@datadog/browser-core'
import type { Observable, TelemetryEvent, RawError, ContextManager } from '@datadog/browser-core'
import {
sendToExtension,
createPageExitObservable,
Expand Down Expand Up @@ -33,13 +33,16 @@ import type { RumConfiguration, RumInitConfiguration } from '../domain/configura
import { serializeRumConfiguration } from '../domain/configuration'
import type { ViewOptions } from '../domain/rumEventsCollection/view/trackViews'
import { startFeatureFlagContexts } from '../domain/contexts/featureFlagContext'
import { startUserDataTelemetry } from '../domain/startUserDataTelemetry'
import type { RecorderApi } from './rumPublicApi'

export function startRum(
initConfiguration: RumInitConfiguration,
configuration: RumConfiguration,
getCommonContext: () => CommonContext,
recorderApi: RecorderApi,
globalContextManager: ContextManager,
userContextManager: ContextManager,
initialViewOptions?: ViewOptions
) {
const lifeCycle = new LifeCycle()
Expand All @@ -65,12 +68,13 @@ export function startRum(
const reportError = (error: RawError) => {
lifeCycle.notify(LifeCycleEventType.RAW_ERROR_COLLECTED, { error })
}
let batch
if (!canUseEventBridge()) {
const pageExitObservable = createPageExitObservable()
pageExitObservable.subscribe((event) => {
lifeCycle.notify(LifeCycleEventType.PAGE_EXITED, event)
})
startRumBatch(configuration, lifeCycle, telemetry.observable, reportError, pageExitObservable)
batch = startRumBatch(configuration, lifeCycle, telemetry.observable, reportError, pageExitObservable)
} else {
startRumEventBridge(lifeCycle)
}
Expand All @@ -90,6 +94,18 @@ export function startRum(
getCommonContext,
reportError
)

if (batch) {
startUserDataTelemetry(
amortemousque marked this conversation as resolved.
Show resolved Hide resolved
configuration,
telemetry,
lifeCycle,
globalContextManager,
userContextManager,
featureFlagContexts,
batch.flushObservable
)
}
addTelemetryConfiguration(serializeRumConfiguration(initConfiguration))

startLongTaskCollection(lifeCycle, session)
Expand Down
2 changes: 2 additions & 0 deletions packages/rum-core/src/domain/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export interface RumConfiguration extends Configuration {
trackResources: boolean | undefined
trackLongTasks: boolean | undefined
version?: string
userDataTelemetrySampleRate: number
}

export function validateAndBuildRumConfiguration(
Expand Down Expand Up @@ -152,6 +153,7 @@ export function validateAndBuildRumConfiguration(
defaultPrivacyLevel: objectHasValue(DefaultPrivacyLevel, initConfiguration.defaultPrivacyLevel)
? initConfiguration.defaultPrivacyLevel
: DefaultPrivacyLevel.MASK_USER_INPUT,
userDataTelemetrySampleRate: 1,
},
baseConfiguration
)
Expand Down
144 changes: 144 additions & 0 deletions packages/rum-core/src/domain/startUserDataTelemetry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import type { BatchFlushEvent, Context, TelemetryEvent } from '@datadog/browser-core'
import {
resetExperimentalFeatures,
updateExperimentalFeatures,
TelemetryService,
startTelemetry,
Observable,
} from '@datadog/browser-core'
import type { TestSetupBuilder } from '../../test/specHelper'
import { setup } from '../../test/specHelper'
import { RumEventType } from '../rawRumEvent.types'
import type { RumEvent } from '../rumEvent.types'
import { LifeCycle, LifeCycleEventType } from './lifeCycle'
import { MEASURES_FLUSH_INTERVAL, startUserDataTelemetry } from './startUserDataTelemetry'

describe('userDataTelemetry', () => {
let setupBuilder: TestSetupBuilder
let batchFlushObservable: Observable<BatchFlushEvent>
let telemetryEvents: TelemetryEvent[]
let fakeContextBytesCount: number
let lifeCycle: LifeCycle

function generateBatch({
eventNumber,
contextBytesCount,
batchBytesCount,
}: {
eventNumber: number
contextBytesCount: number
batchBytesCount: number
}) {
fakeContextBytesCount = contextBytesCount
for (let index = 0; index < eventNumber; index++) {
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { type: RumEventType.VIEW } as RumEvent & Context)
}
batchFlushObservable.notify({ bufferBytesCount: batchBytesCount, bufferMessagesCount: eventNumber })
}

beforeEach(() => {
updateExperimentalFeatures(['user_data_telemetry'])
setupBuilder = setup()
.withFakeClock()
.withConfiguration({ telemetrySampleRate: 100, userDataTelemetrySampleRate: 100, maxTelemetryEventsPerPage: 2 })
.beforeBuild(({ globalContextManager, userContextManager, featureFlagContexts, configuration }) => {
batchFlushObservable = new Observable()
lifeCycle = new LifeCycle()
fakeContextBytesCount = 1

spyOn(globalContextManager, 'getBytesCount').and.callFake(() => fakeContextBytesCount)
spyOn(userContextManager, 'getBytesCount').and.callFake(() => fakeContextBytesCount)
spyOn(featureFlagContexts, 'getFeatureFlagBytesCount').and.callFake(() => fakeContextBytesCount)

telemetryEvents = []
const telemetry = startTelemetry(TelemetryService.RUM, configuration)
telemetry.observable.subscribe((telemetryEvent) => telemetryEvents.push(telemetryEvent))

startUserDataTelemetry(
configuration,
telemetry,
lifeCycle,
globalContextManager,
userContextManager,
featureFlagContexts,
batchFlushObservable
)
})
})

afterEach(() => {
setupBuilder.cleanup()
resetExperimentalFeatures()
})

it('should collect user data telemetry', () => {
const { clock } = setupBuilder.build()

generateBatch({ eventNumber: 2, contextBytesCount: 2, batchBytesCount: 2 })
generateBatch({ eventNumber: 1, contextBytesCount: 1, batchBytesCount: 1 })
clock.tick(MEASURES_FLUSH_INTERVAL)

expect(telemetryEvents[0].telemetry).toEqual({
type: 'log',
status: 'debug',
message: 'User data measures',
batchCount: 2,
batchBytesCount: { min: 1, max: 2, sum: 3 },
batchMessagesCount: { min: 1, max: 2, sum: 3 },
globalContextBytes: { min: 1, max: 2, sum: 5 },
userContextBytes: { min: 1, max: 2, sum: 5 },
featureFlagBytes: { min: 1, max: 2, sum: 5 },
})
})

it('should not collect empty contexts telemetry', () => {
const { clock } = setupBuilder.build()

generateBatch({ eventNumber: 1, contextBytesCount: 0, batchBytesCount: 1 })
clock.tick(MEASURES_FLUSH_INTERVAL)

expect(telemetryEvents[0].telemetry.globalContextBytes).not.toBeDefined()
expect(telemetryEvents[0].telemetry.userContextBytes).not.toBeDefined()
expect(telemetryEvents[0].telemetry.featureFlagBytes).not.toBeDefined()
})

it('should not collect contexts telemetry of a unfinished batches', () => {
const { clock } = setupBuilder.build()

generateBatch({ eventNumber: 1, contextBytesCount: 1, batchBytesCount: 1 })
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { type: RumEventType.VIEW } as RumEvent & Context)
clock.tick(MEASURES_FLUSH_INTERVAL)

expect(telemetryEvents[0].telemetry).toEqual(
jasmine.objectContaining({
batchCount: 1,
batchBytesCount: { min: 1, max: 1, sum: 1 },
batchMessagesCount: { min: 1, max: 1, sum: 1 },
globalContextBytes: { min: 1, max: 1, sum: 1 },
userContextBytes: { min: 1, max: 1, sum: 1 },
featureFlagBytes: { min: 1, max: 1, sum: 1 },
})
)
})

it('should not collect user data telemetry when telemetry disabled', () => {
const { clock } = setupBuilder
.withConfiguration({ telemetrySampleRate: 100, userDataTelemetrySampleRate: 0 })
.build()

generateBatch({ eventNumber: 1, contextBytesCount: 1, batchBytesCount: 1 })
clock.tick(MEASURES_FLUSH_INTERVAL)

expect(telemetryEvents.length).toEqual(0)
})

it('should not collect user data telemetry when user_data_telemetry ff is disabled', () => {
resetExperimentalFeatures()
const { clock } = setupBuilder.build()

generateBatch({ eventNumber: 1, contextBytesCount: 1, batchBytesCount: 1 })
clock.tick(MEASURES_FLUSH_INTERVAL)

expect(telemetryEvents.length).toEqual(0)
})
})