Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/core/src/domain/bufferedData.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { registerCleanupTask } from '../../test'
import { replaceMockableWithSpy, registerCleanupTask } from '../../test'
import { Observable } from '../tools/observable'
import { clocksNow } from '../tools/utils/timeUtils'
import { BufferedDataType, startBufferingData } from './bufferedData'
import { ErrorHandling, ErrorSource, type RawError } from './error/error.types'
import { trackRuntimeError } from './error/trackRuntimeError'

describe('startBufferingData', () => {
it('collects runtime errors', (done) => {
const runtimeErrorObservable = new Observable<RawError>()
const { observable, stop } = startBufferingData(() => runtimeErrorObservable)
replaceMockableWithSpy(trackRuntimeError).and.returnValue(runtimeErrorObservable)
const { observable, stop } = startBufferingData()
registerCleanupTask(stop)

const rawError = {
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/domain/bufferedData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BufferedObservable } from '../tools/observable'
import { mockable } from '../tools/mockable'
import type { RawError } from './error/error.types'
import { trackRuntimeError } from './error/trackRuntimeError'

Expand All @@ -13,10 +14,10 @@ export interface BufferedData {
error: RawError
}

export function startBufferingData(trackRuntimeErrorImpl = trackRuntimeError) {
export function startBufferingData() {
const observable = new BufferedObservable<BufferedData>(BUFFER_LIMIT)

const runtimeErrorSubscription = trackRuntimeErrorImpl().subscribe((error) => {
const runtimeErrorSubscription = mockable(trackRuntimeError)().subscribe((error) => {
observable.notify({
type: BufferedDataType.RUNTIME_ERROR,
error,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export * from './tools/utils/browserDetection'
export { sendToExtension } from './tools/sendToExtension'
export { runOnReadyState, asyncRunOnReadyState } from './browser/runOnReadyState'
export { getZoneJsOriginalValue } from './tools/getZoneJsOriginalValue'
export { mockable } from './tools/mockable'
export type { InstrumentedMethodCall } from './tools/instrumentMethod'
export { instrumentMethod, instrumentSetter } from './tools/instrumentMethod'
export {
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/tools/mockable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
declare const __BUILD_ENV__SDK_VERSION__: string

export const mockableValues = new Map<any, any>()

/**
* Wraps a value to make it mockable in tests. In production builds, this is a no-op
* that returns the value as-is. In test builds, it checks if a mock replacement has
* been registered and returns that instead.
*
* @example
* // In source file:
* import { mockable } from '../tools/mockable'
* export const getNavigationEntry = mockable(() => performance.getEntriesByType('navigation')[0])
*
* // In test file:
* import { mockValue } from '@datadog/browser-core/test'
* mockValue(getNavigationEntry, () => FAKE_NAVIGATION_ENTRY)
*/
export function mockable<T>(value: T): T {
// In test builds, return a wrapper that checks for mocks at call time
if (__BUILD_ENV__SDK_VERSION__ === 'test' && mockableValues.get(value)) {
return mockableValues.get(value)! as T
}
// In production, return the value as-is
return value
}
1 change: 1 addition & 0 deletions packages/core/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ export * from './consoleLog'
export * from './createHooks'
export * from './fakeSessionStoreStrategy'
export * from './readFormData'
export * from './mock'
42 changes: 42 additions & 0 deletions packages/core/test/mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { mockableValues } from '../src/tools/mockable'
import { registerCleanupTask } from './registerCleanupTask'

/**
* Registers a mock replacement for a mockable value. The mock is automatically
* cleaned up after each test via registerCleanupTask.
*
* @param value - The original value (must be the same reference passed to mockable())
* @param replacement - The mock replacement
* @example
* import { mockValue } from '@datadog/browser-core/test'
* import { trackRuntimeError } from '../domain/error/trackRuntimeError'
*
* mockValue(trackRuntimeError, () => new Observable<RawError>())
*/
export function replaceMockable<T>(value: T, replacement: T): void {
mockableValues.set(value, replacement)
registerCleanupTask(() => {
mockableValues.delete(value)
})
}

/**
* Creates a Jasmine spy and registers it as a mock replacement for a mockable function.
* The mock is automatically cleaned up after each test via registerCleanupTask.
*
* @param value - The original function (must be the same reference passed to mockable())
* @returns A Jasmine spy that can be used for assertions
* @example
* import { mockWithSpy } from '@datadog/browser-core/test'
* import { trackRuntimeError } from '../domain/error/trackRuntimeError'
*
* const spy = mockWithSpy(trackRuntimeError)
* spy.and.returnValue(new Observable<RawError>())
* // ... test code ...
* expect(spy).toHaveBeenCalled()
*/
export function replaceMockableWithSpy<T extends (...args: any[]) => any>(value: T): jasmine.Spy<T> {
const spy = jasmine.createSpy<T>()
replaceMockable(value, spy as unknown as T)
return spy
}
11 changes: 7 additions & 4 deletions packages/logs/src/boot/logsPublicApi.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { ContextManager } from '@datadog/browser-core'
import { monitor, display, createContextManager, TrackingConsent } from '@datadog/browser-core'
import { monitor, display, createContextManager, TrackingConsent, startTelemetry } from '@datadog/browser-core'
import { HandlerType } from '../domain/logger'
import { StatusType } from '../domain/logger/isAuthorized'
import { createFakeTelemetryObject } from '../../../core/test'
import { createFakeTelemetryObject, replaceMockableWithSpy } from '../../../core/test'
import type { LogsPublicApi } from './logsPublicApi'
import { makeLogsPublicApi } from './logsPublicApi'
import type { StartLogs, StartLogsResult } from './startLogs'
import { startLogs } from './startLogs'

const DEFAULT_INIT_CONFIGURATION = { clientToken: 'xxx', trackingConsent: TrackingConsent.GRANTED }

Expand Down Expand Up @@ -242,7 +243,7 @@ function makeLogsPublicApiWithDefaults({
startLogsResult?: Partial<StartLogsResult>
} = {}) {
const handleLogSpy = jasmine.createSpy<StartLogsResult['handleLog']>()
const startLogsSpy = jasmine.createSpy<StartLogs>().and.callFake(() => ({
const startLogsSpy = replaceMockableWithSpy(startLogs).and.callFake(() => ({
handleLog: handleLogSpy,
getInternalContext,
accountContext: {} as any,
Expand All @@ -257,9 +258,11 @@ function makeLogsPublicApiWithDefaults({
return { message, logger, savedCommonContext, savedDate }
}

replaceMockableWithSpy(startTelemetry).and.callFake(createFakeTelemetryObject)

return {
startLogsSpy,
logsPublicApi: makeLogsPublicApi(startLogsSpy, createFakeTelemetryObject),
logsPublicApi: makeLogsPublicApi(),
getLoggedMessage,
}
}
21 changes: 7 additions & 14 deletions packages/logs/src/boot/logsPublicApi.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import type {
TrackingConsent,
PublicApi,
ContextManager,
Account,
Context,
User,
startTelemetry,
} from '@datadog/browser-core'
import type { TrackingConsent, PublicApi, ContextManager, Account, Context, User } from '@datadog/browser-core'
import {
ContextManagerMethod,
CustomerContextKey,
Expand All @@ -20,14 +12,16 @@ import {
defineContextMethod,
startBufferingData,
callMonitored,
mockable,
} from '@datadog/browser-core'
import type { LogsInitConfiguration } from '../domain/configuration'
import type { HandlerType } from '../domain/logger'
import type { StatusType } from '../domain/logger/isAuthorized'
import { Logger } from '../domain/logger'
import { buildCommonContext } from '../domain/contexts/commonContext'
import type { InternalContext } from '../domain/contexts/internalContext'
import type { StartLogs, StartLogsResult } from './startLogs'
import type { StartLogsResult } from './startLogs'
import { startLogs } from './startLogs'
import { createPreStartStrategy } from './preStartLogs'

export interface LoggerConfiguration {
Expand Down Expand Up @@ -273,15 +267,15 @@ export interface Strategy {
handleLog: StartLogsResult['handleLog']
}

export function makeLogsPublicApi(startLogsImpl: StartLogs, startTelemetryImpl?: typeof startTelemetry): LogsPublicApi {
export function makeLogsPublicApi(): LogsPublicApi {
const trackingConsentState = createTrackingConsentState()
const bufferedDataObservable = startBufferingData().observable

let strategy = createPreStartStrategy(
buildCommonContext,
trackingConsentState,
(initConfiguration, configuration, hooks) => {
const startLogsResult = startLogsImpl(
const startLogsResult = mockable(startLogs)(
configuration,
buildCommonContext,
trackingConsentState,
Expand All @@ -291,8 +285,7 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs, startTelemetryImpl?:

strategy = createPostStartStrategy(initConfiguration, startLogsResult)
return startLogsResult
},
startTelemetryImpl
}
)

const getStrategy = () => strategy
Expand Down
6 changes: 4 additions & 2 deletions packages/logs/src/boot/preStartLogs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
mockClock,
mockEventBridge,
createFakeTelemetryObject,
replaceMockableWithSpy,
} from '@datadog/browser-core/test'
import type { TimeStamp, TrackingConsentState } from '@datadog/browser-core'
import {
Expand All @@ -12,6 +13,7 @@ import {
createTrackingConsentState,
display,
resetFetchObservable,
startTelemetry,
} from '@datadog/browser-core'
import type { CommonContext } from '../rawLogsEvent.types'
import type { HybridInitConfiguration, LogsInitConfiguration } from '../domain/configuration'
Expand Down Expand Up @@ -285,10 +287,10 @@ function createPreStartStrategyWithDefaults({
handleLog: handleLogSpy,
} as unknown as StartLogsResult)
const getCommonContextSpy = jasmine.createSpy<() => CommonContext>()
const startTelemetrySpy = jasmine.createSpy().and.callFake(createFakeTelemetryObject)
const startTelemetrySpy = replaceMockableWithSpy(startTelemetry).and.callFake(createFakeTelemetryObject)

return {
strategy: createPreStartStrategy(getCommonContextSpy, trackingConsentState, doStartLogsSpy, startTelemetrySpy),
strategy: createPreStartStrategy(getCommonContextSpy, trackingConsentState, doStartLogsSpy),
startTelemetrySpy,
handleLogSpy,
doStartLogsSpy,
Expand Down
6 changes: 3 additions & 3 deletions packages/logs/src/boot/preStartLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
buildUserContextManager,
startTelemetry,
TelemetryService,
mockable,
} from '@datadog/browser-core'
import type { Hooks } from '../domain/hooks'
import { createHooks } from '../domain/hooks'
Expand All @@ -34,8 +35,7 @@ export type DoStartLogs = (
export function createPreStartStrategy(
getCommonContext: () => CommonContext,
trackingConsentState: TrackingConsentState,
doStartLogs: DoStartLogs,
startTelemetryImpl = startTelemetry
doStartLogs: DoStartLogs
): Strategy {
const bufferApiCalls = createBoundedBuffer<StartLogsResult>()

Expand All @@ -59,7 +59,7 @@ export function createPreStartStrategy(
return
}

startTelemetryImpl(TelemetryService.LOGS, cachedConfiguration, hooks)
mockable(startTelemetry)(TelemetryService.LOGS, cachedConfiguration, hooks)

trackingConsentStateSubscription.unsubscribe()
const startLogsResult = doStartLogs(cachedInitConfiguration, cachedConfiguration, hooks)
Expand Down
3 changes: 1 addition & 2 deletions packages/logs/src/entries/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import { defineGlobal, getGlobalObject } from '@datadog/browser-core'
import type { LogsPublicApi } from '../boot/logsPublicApi'
import { makeLogsPublicApi } from '../boot/logsPublicApi'
import { startLogs } from '../boot/startLogs'

export type { InternalContext } from '../domain/contexts/internalContext'
export type { LogsMessage } from '../domain/logger'
Expand Down Expand Up @@ -57,7 +56,7 @@ export type {
* @see {@link DatadogLogs}
* @see [Browser Log Collection](https://docs.datadoghq.com/logs/log_collection/javascript/)
*/
export const datadogLogs = makeLogsPublicApi(startLogs)
export const datadogLogs = makeLogsPublicApi()

interface BrowserWindow extends Window {
DD_LOGS?: LogsPublicApi
Expand Down
7 changes: 4 additions & 3 deletions packages/rum-core/src/boot/preStartRum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DefaultPrivacyLevel,
resetFetchObservable,
ExperimentalFeature,
startTelemetry,
} from '@datadog/browser-core'
import type { Clock } from '@datadog/browser-core/test'
import {
Expand All @@ -20,6 +21,7 @@ import {
mockSyntheticsWorkerValues,
mockExperimentalFeatures,
createFakeTelemetryObject,
replaceMockableWithSpy,
} from '@datadog/browser-core/test'
import type { HybridInitConfiguration, RumInitConfiguration } from '../domain/configuration'
import type { ViewOptions } from '../domain/view/trackViews'
Expand Down Expand Up @@ -863,14 +865,13 @@ function createPreStartStrategyWithDefaults({
trackingConsentState?: TrackingConsentState
} = {}) {
const doStartRumSpy = jasmine.createSpy<DoStartRum>()
const startTelemetrySpy = jasmine.createSpy().and.callFake(createFakeTelemetryObject)
const startTelemetrySpy = replaceMockableWithSpy(startTelemetry).and.callFake(createFakeTelemetryObject)
return {
strategy: createPreStartStrategy(
rumPublicApiOptions,
trackingConsentState,
createCustomVitalsState(),
doStartRumSpy,
startTelemetrySpy
doStartRumSpy
),
doStartRumSpy,
startTelemetrySpy,
Expand Down
6 changes: 3 additions & 3 deletions packages/rum-core/src/boot/preStartRum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
sanitize,
startTelemetry,
TelemetryService,
mockable,
} from '@datadog/browser-core'
import type { Hooks } from '../domain/hooks'
import { createHooks } from '../domain/hooks'
Expand Down Expand Up @@ -60,8 +61,7 @@ export function createPreStartStrategy(
{ ignoreInitIfSyntheticsWillInjectRum = true, startDeflateWorker }: RumPublicApiOptions,
trackingConsentState: TrackingConsentState,
customVitalsState: CustomVitalsState,
doStartRum: DoStartRum,
startTelemetryImpl = startTelemetry
doStartRum: DoStartRum
): Strategy {
const bufferApiCalls = createBoundedBuffer<StartRumResult>()

Expand Down Expand Up @@ -96,7 +96,7 @@ export function createPreStartStrategy(

// Start telemetry only once, when we have consent and configuration
if (!telemetry) {
telemetry = startTelemetryImpl(TelemetryService.RUM, cachedConfiguration, hooks)
telemetry = mockable(startTelemetry)(TelemetryService.RUM, cachedConfiguration, hooks)
}

trackingConsentStateSubscription.unsubscribe()
Expand Down
Loading