diff --git a/eslint-local-rules/disallowSideEffects.js b/eslint-local-rules/disallowSideEffects.js index d7bfb91f1f..2c543ab88a 100644 --- a/eslint-local-rules/disallowSideEffects.js +++ b/eslint-local-rules/disallowSideEffects.js @@ -30,12 +30,18 @@ const pathsWithSideEffect = new Set([ `${packagesRoot}/flagging/src/entries/main.ts`, `${packagesRoot}/rum/src/entries/main.ts`, `${packagesRoot}/rum-slim/src/entries/main.ts`, + `${packagesRoot}/rum-next/src/entries/bundle.ts`, + `${packagesRoot}/core-next/src/entries/bundle.ts`, + `${packagesRoot}/core-next/src/entries/main.ts`, ]) // Those packages are known to have no side effects when evaluated const packagesWithoutSideEffect = new Set([ '@datadog/browser-core', '@datadog/browser-rum-core', + '@datadog/browser-internal-next', + '@datadog/browser-rum/internal', + '@datadog/browser-logs/internal', 'react', 'react-router-dom', ]) @@ -184,6 +190,11 @@ function isAllowedCallExpression({ callee }) { return true } + // Allow "Symbol.for()" + if (callee.type === 'MemberExpression' && callee.object.name === 'Symbol' && callee.property.name === 'for') { + return true + } + return false } diff --git a/eslint.config.mjs b/eslint.config.mjs index 600ddf8f45..944f05f6ea 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -418,7 +418,7 @@ export default tseslint.config( }, { - files: ['packages/{rum,logs,flagging,rum-slim}/src/entries/*.ts'], + files: ['packages/{rum,logs,flagging,rum-slim}/src/entries/main.ts'], rules: { 'local-rules/disallow-enum-exports': 'error', }, diff --git a/packages/core-next/package.json b/packages/core-next/package.json new file mode 100644 index 0000000000..ff25f0eacd --- /dev/null +++ b/packages/core-next/package.json @@ -0,0 +1,20 @@ +{ + "name": "@datadog/browser-core-next", + "private": true, + "main": "cjs/entries/main.js", + "module": "esm/entries/main.js", + "types": "cjs/entries/main.d.ts", + "scripts": { + "build": "node ../../scripts/build/build-package.ts --modules --bundle datadog-core-next.js", + "build:bundle": "node ../../scripts/build/build-package.ts --bundle datadog-core-next.js" + }, + "dependencies": { + "@datadog/browser-core": "6.25.0", + "@datadog/browser-internal-next": "workspace:*", + "@datadog/browser-logs-next": "workspace:*", + "@datadog/browser-profiling-next": "workspace:*", + "@datadog/browser-rum": "6.25.0", + "@datadog/browser-rum-core": "6.25.0", + "@datadog/browser-rum-next": "workspace:*" + } +} diff --git a/packages/core-next/src/entries/bundle.ts b/packages/core-next/src/entries/bundle.ts new file mode 100644 index 0000000000..0a222d53ad --- /dev/null +++ b/packages/core-next/src/entries/bundle.ts @@ -0,0 +1,6 @@ +import { definePublicApiGlobal } from '@datadog/browser-internal-next' +import * as main from './main' + +export * from './main' + +definePublicApiGlobal(main) diff --git a/packages/core-next/src/entries/main.ts b/packages/core-next/src/entries/main.ts new file mode 100644 index 0000000000..3cbc1e3616 --- /dev/null +++ b/packages/core-next/src/entries/main.ts @@ -0,0 +1,76 @@ +import { CoreContextType as CoreContextType, getInternalApi, MessageType } from '@datadog/browser-internal-next' +import { addEventListener } from '@datadog/browser-core' + +export { initialize } from '../initialize' + +// TODO: move this somewhere else +// We don't use `trackRuntimeError` from browser-core because we don't want to deal with raw errors +// parsing at this layer. +// Also, let's try to use event listeners instead of intstrumenting handlers, as it sounds cleaner +// and smaller than bringing instrumentation tooling. +function trackRuntimeErrors() { + addEventListener({}, globalThis, 'error', (event) => { + getInternalApi().notify({ + type: MessageType.RUNTIME_ERROR, + error: event.error, + event, + }) + }) + + addEventListener({}, globalThis, 'unhandledrejection', (event) => { + getInternalApi().notify({ + type: MessageType.RUNTIME_ERROR, + error: event.reason || 'Empty reason', + }) + }) +} + +trackRuntimeErrors() + +export function setGlobalContext(value: object) { + setContext(CoreContextType.GLOBAL, value) +} + +export function setGlobalContextProperty(key: string, value: unknown) { + setContextProperty(CoreContextType.GLOBAL, key, value) +} + +export function clearGlobalContext() { + clearContext(CoreContextType.GLOBAL) +} + +export function setAccount(value: object) { + setContext(CoreContextType.ACCOUNT, value) +} + +export function setAccountProperty(key: string, value: unknown) { + setContextProperty(CoreContextType.ACCOUNT, key, value) +} + +export function clearAccount() { + clearContext(CoreContextType.ACCOUNT) +} + +export function setUser(user: object) { + setContext(CoreContextType.USER, user) +} + +export function setUserProperty(key: string, value: unknown) { + setContextProperty(CoreContextType.USER, key, value) +} + +export function clearUser() { + clearContext(CoreContextType.USER) +} + +function setContext(context: CoreContextType, value: object) { + getInternalApi().notify({ type: MessageType.CORE_SET_CONTEXT, context, value }) +} + +function setContextProperty(context: CoreContextType, key: string, value: unknown) { + getInternalApi().notify({ type: MessageType.CORE_SET_CONTEXT_PROPERTY, context, key, value }) +} + +function clearContext(context: CoreContextType) { + getInternalApi().notify({ type: MessageType.CORE_CLEAR_CONTEXT, context }) +} diff --git a/packages/core-next/src/initialize.ts b/packages/core-next/src/initialize.ts new file mode 100644 index 0000000000..4cfe194698 --- /dev/null +++ b/packages/core-next/src/initialize.ts @@ -0,0 +1,44 @@ +import { display, monitorError } from '@datadog/browser-core' +import { getInternalApi } from '@datadog/browser-internal-next' +import type { CoreInitializeConfiguration } from '@datadog/browser-internal-next' + +export function initialize(initializeConfiguration: CoreInitializeConfiguration) { + const promises = [ + import('./lazy'), + import('./lazyCompression'), + initializeConfiguration.rum && import('@datadog/browser-rum-next/lazy'), + initializeConfiguration.profiling && import('@datadog/browser-profiling-next/lazy'), + initializeConfiguration.logs && import('@datadog/browser-logs-next/lazy'), + ] as const + + // eslint-disable-next-line @typescript-eslint/await-thenable + Promise.all(promises) + .then(([lazyCoreModule, lazyCompressionModule, rumModule, profilingModule, logsModule]) => { + const lazyApi = lazyCoreModule.initialize( + initializeConfiguration, + lazyCompressionModule.initialize(initializeConfiguration)?.createEncoder + ) + if (!lazyApi) { + return + } + + if (rumModule) { + rumModule.initialize(lazyApi) + } + + if (profilingModule) { + profilingModule.initialize(lazyApi) + } + + if (logsModule) { + logsModule.initialize(lazyApi) + } + }) + .finally(() => { + getInternalApi().bus.unbuffer() + }) + .catch((error) => { + display.error('Failed to load lazy chunks', error) + monitorError(error) + }) +} diff --git a/packages/core-next/src/lazy.ts b/packages/core-next/src/lazy.ts new file mode 100644 index 0000000000..0234dfda3c --- /dev/null +++ b/packages/core-next/src/lazy.ts @@ -0,0 +1,156 @@ +import type { + Configuration, + DeflateEncoderStreamId, + Encoder, + EndpointBuilder, + TrackingConsentState, +} from '@datadog/browser-core' +import { + abstractHooks, + buildAccountContextManager, + buildGlobalContextManager, + buildUserContextManager, + createBatch, + createFlushController, + createHttpRequest, + createIdentityEncoder, + createPageMayExitObservable, + createTrackingConsentState, + startSessionManager, + startTelemetry, + TelemetryService, + validateAndBuildConfiguration, +} from '@datadog/browser-core' +import type { CoreInitializeConfiguration, CoreSessionManager } from '@datadog/browser-internal-next' +import { CoreContextType, getInternalApi, MessageType } from '@datadog/browser-internal-next' +import { SessionReplayState } from '@datadog/browser-rum-core' + +export function initialize( + initializeConfiguration: CoreInitializeConfiguration, + createEncoder: (streamId: DeflateEncoderStreamId) => Encoder = createIdentityEncoder +) { + const configuration = validateAndBuildConfiguration(initializeConfiguration) + if (!configuration) { + return + } + + const internalApi = getInternalApi() + const hooks = abstractHooks() // TODO: specialized hook + const contexts = startContexts() + + const reportError = () => { + // TODO + } + const pageMayExitObservable = createPageMayExitObservable(configuration) + const telemetry = startTelemetry( + TelemetryService.RUM, + configuration, + hooks, + reportError, + pageMayExitObservable, + createIdentityEncoder // Keep using the identity encoder here, so we can sent telemetry even if deflate isn't working + ) + + const trackingConsentState = createTrackingConsentState() + + // TODO: handle bridge + + const sessionManager = startCoreSessionManager(configuration, trackingConsentState) + + return { + coreInitializeConfiguration: initializeConfiguration, + createEncoder, + internalApi, + hooks, + contexts, + telemetry, + sessionManager, + createBatch: (endpoints: EndpointBuilder[]) => + createBatch({ + encoder: createEncoder( + Math.random() as any // TODO: remove named stream id + ), + request: createHttpRequest(endpoints, reportError), + flushController: createFlushController({ + pageMayExitObservable, + sessionExpireObservable: sessionManager.expireObservable, + }), + }), + } +} + +function startContexts() { + const global = buildGlobalContextManager() + const user = buildUserContextManager() + const account = buildAccountContextManager() + + const contextsByType = { + [CoreContextType.GLOBAL]: global, + [CoreContextType.USER]: user, + [CoreContextType.ACCOUNT]: account, + } + + getInternalApi().bus.subscribe(({ message }) => { + switch (message.type) { + case MessageType.CORE_SET_CONTEXT: + contextsByType[message.context].setContext(message.value) + break + + case MessageType.CORE_SET_CONTEXT_PROPERTY: + contextsByType[message.context].setContextProperty(message.key, message.value) + break + + case MessageType.CORE_CLEAR_CONTEXT: + contextsByType[message.context].clearContext() + break + } + }) + + return { + global, + user, + account, + } +} + +function startCoreSessionManager( + configuration: Configuration, + trackingConsentState: TrackingConsentState +): CoreSessionManager { + // TODO: we should use a fallback if: + // * there is an event bridge + // * configuration.sessionStoreStrategyType is undefined (ex: no cookie access) + + const sessionManager = startSessionManager( + configuration, + // TODO: product type will be removed in the future session manager + 'x', + () => 'x', + trackingConsentState + ) + + sessionManager.sessionStateUpdateObservable.subscribe(({ previousState, newState }) => { + if (!previousState.forcedReplay && newState.forcedReplay) { + const sessionEntity = sessionManager.findSession() + if (sessionEntity) { + sessionEntity.isReplayForced = true + } + } + }) + + return { + ...sessionManager, + findTrackedSession: (startTime) => { + const session = sessionManager.findSession(startTime) + if (!session) { + return + } + return { + id: session.id, + sessionReplay: SessionReplayState.OFF, // TODO + anonymousId: session.anonymousId, + } + }, + setForcedReplay: () => sessionManager.updateSessionState({ forcedReplay: '1' }), + } +} diff --git a/packages/core-next/src/lazyCompression.ts b/packages/core-next/src/lazyCompression.ts new file mode 100644 index 0000000000..57600f8047 --- /dev/null +++ b/packages/core-next/src/lazyCompression.ts @@ -0,0 +1,21 @@ +import type { DeflateEncoderStreamId } from '@datadog/browser-core' +import { noop } from '@datadog/browser-core' +import type { CoreInitializeConfiguration } from '@datadog/browser-internal-next' +import { createDeflateEncoder, startDeflateWorker } from '@datadog/browser-rum/internal' + +export function initialize(configuration: CoreInitializeConfiguration) { + const deflateWorker = startDeflateWorker( + configuration as any, + 'Datadog Session Replay', + // Report worker creation failure? + noop + ) + if (!deflateWorker) { + return + } + + return { + createEncoder: (streamId: DeflateEncoderStreamId) => + createDeflateEncoder(configuration as any, deflateWorker, streamId), + } +} diff --git a/packages/core-next/typedoc.json b/packages/core-next/typedoc.json new file mode 100644 index 0000000000..002b26a53c --- /dev/null +++ b/packages/core-next/typedoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/entries/main.ts"] +} diff --git a/packages/core/src/browser/addEventListener.ts b/packages/core/src/browser/addEventListener.ts index 6fb2c43ba5..011f818a70 100644 --- a/packages/core/src/browser/addEventListener.ts +++ b/packages/core/src/browser/addEventListener.ts @@ -39,6 +39,7 @@ export const enum DOM_EVENT { SECURITY_POLICY_VIOLATION = 'securitypolicyviolation', SELECTION_CHANGE = 'selectionchange', STORAGE = 'storage', + ERROR = 'error', } interface AddEventListenerOptions { @@ -47,7 +48,7 @@ interface AddEventListenerOptions { passive?: boolean } -type EventMapFor = T extends Window +type EventMapFor = T extends Window | typeof globalThis ? WindowEventMap & { // TS 4.9.5 does not support `freeze` and `resume` events yet freeze: Event diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index afe4af85c9..a13e293c1f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -21,7 +21,7 @@ export { } from './tools/experimentalFeatures' export { trackRuntimeError } from './domain/error/trackRuntimeError' export type { StackTrace } from './tools/stackTrace/computeStackTrace' -export { computeStackTrace } from './tools/stackTrace/computeStackTrace' +export { computeStackTrace, computeStackTraceFromOnErrorMessage } from './tools/stackTrace/computeStackTrace' export type { PublicApi } from './boot/init' export { defineGlobal, makePublicApi } from './boot/init' export { displayAlreadyInitializedError } from './boot/displayAlreadyInitializedError' @@ -59,7 +59,15 @@ export { SESSION_NOT_TRACKED, SessionPersistence, } from './domain/session/sessionConstants' -export type { BandwidthStats, HttpRequest, HttpRequestEvent, Payload, FlushEvent, FlushReason } from './transport' +export type { + BandwidthStats, + HttpRequest, + HttpRequestEvent, + Payload, + FlushEvent, + FlushReason, + Batch, +} from './transport' export { createHttpRequest, canUseEventBridge, diff --git a/packages/core/src/tools/monitor.ts b/packages/core/src/tools/monitor.ts index aa08f6ee36..cfbee66ceb 100644 --- a/packages/core/src/tools/monitor.ts +++ b/packages/core/src/tools/monitor.ts @@ -1,7 +1,7 @@ import { display } from './display' let onMonitorErrorCollected: undefined | ((error: unknown) => void) -let debugMode = false +let debugMode = true export function startMonitorErrorCollection(newOnMonitorErrorCollected: (error: unknown) => void) { onMonitorErrorCollected = newOnMonitorErrorCollected diff --git a/packages/core/src/transport/index.ts b/packages/core/src/transport/index.ts index ce6307356c..b8f6901bfb 100644 --- a/packages/core/src/transport/index.ts +++ b/packages/core/src/transport/index.ts @@ -3,5 +3,6 @@ export { createHttpRequest } from './httpRequest' export type { BrowserWindowWithEventBridge, DatadogEventBridge } from './eventBridge' export { canUseEventBridge, bridgeSupports, getEventBridge, BridgeCapability } from './eventBridge' export { createBatch } from './batch' +export type { Batch } from './batch' export type { FlushController, FlushEvent, FlushReason } from './flushController' export { createFlushController, FLUSH_DURATION_LIMIT } from './flushController' diff --git a/packages/internal-next/package.json b/packages/internal-next/package.json new file mode 100644 index 0000000000..41bf03ef07 --- /dev/null +++ b/packages/internal-next/package.json @@ -0,0 +1,16 @@ +{ + "name": "@datadog/browser-internal-next", + "private": true, + "main": "cjs/entries/main.js", + "module": "esm/entries/main.js", + "types": "cjs/entries/main.d.ts", + "sideEffects": false, + "scripts": { + "build": "node ../../scripts/build/build-package.ts --modules" + }, + "dependencies": { + "@datadog/browser-core": "6.25.0", + "@datadog/browser-logs": "6.25.0", + "@datadog/browser-rum-core": "6.25.0" + } +} diff --git a/packages/internal-next/src/bindContextManager.ts b/packages/internal-next/src/bindContextManager.ts new file mode 100644 index 0000000000..75bee84263 --- /dev/null +++ b/packages/internal-next/src/bindContextManager.ts @@ -0,0 +1,19 @@ +import { isEmptyObject } from '@datadog/browser-core' +import type { ContextManager } from '@datadog/browser-core' + +// This is temporary. Logs and RUM functions are creating their own context manager, but in the +// future we should use the ones created in core. This function forwards all changes from one +// context manager to the other. +export function bindContextManager(inputContext: ContextManager, outputContext: ContextManager) { + updateContext() + inputContext.changeObservable.subscribe(updateContext) + + function updateContext() { + const context = inputContext.getContext() + if (!isEmptyObject(context)) { + outputContext.setContext(context) + } else { + outputContext.clearContext() + } + } +} diff --git a/packages/internal-next/src/bufferedDataFromMessageBus.ts b/packages/internal-next/src/bufferedDataFromMessageBus.ts new file mode 100644 index 0000000000..801adf95aa --- /dev/null +++ b/packages/internal-next/src/bufferedDataFromMessageBus.ts @@ -0,0 +1,44 @@ +import type { BufferedData, StackTrace } from '@datadog/browser-core' +import { + BufferedDataType, + computeRawError, + computeStackTraceFromOnErrorMessage, + ErrorHandling, + ErrorSource, + isError, + NonErrorPrefix, + Observable, +} from '@datadog/browser-core' +import { MessageType } from './internalApi' +import type { MessageEnvelope } from './internalApi' + +// This is temporary. In the future, the buffered data observable should be removed in favor of the +// message bus. +export function createBufferedDataFromMessageBus(bus: Observable) { + const bufferedDataObservable = new Observable() + bus.subscribe(({ clocks, message }) => { + switch (message.type) { + case MessageType.RUNTIME_ERROR: { + let stackTrace: StackTrace | undefined + if (!isError(message.error) && message.event) { + const event = message.event + stackTrace = computeStackTraceFromOnErrorMessage(event.message, event.filename, event.lineno, event.colno) + } + bufferedDataObservable.notify({ + type: BufferedDataType.RUNTIME_ERROR, + error: computeRawError({ + stackTrace, + originalError: message.error, + startClocks: clocks, + nonErrorPrefix: NonErrorPrefix.UNCAUGHT, + source: ErrorSource.SOURCE, + handling: ErrorHandling.UNHANDLED, + }), + }) + break + } + } + }) + + return bufferedDataObservable +} diff --git a/packages/internal-next/src/constants.ts b/packages/internal-next/src/constants.ts new file mode 100644 index 0000000000..1053a643db --- /dev/null +++ b/packages/internal-next/src/constants.ts @@ -0,0 +1,3 @@ +declare const __BUILD_ENV__SDK_VERSION__: string + +export const SDK_VERSION = __BUILD_ENV__SDK_VERSION__ diff --git a/packages/internal-next/src/entries/main.ts b/packages/internal-next/src/entries/main.ts new file mode 100644 index 0000000000..651e933ce9 --- /dev/null +++ b/packages/internal-next/src/entries/main.ts @@ -0,0 +1,7 @@ +export { display } from '@datadog/browser-core' +export type * from '../types' +export * from '../constants' +export * from '../internalApi' +export * from '../publicApi' +export * from '../bufferedDataFromMessageBus' +export * from '../bindContextManager' diff --git a/packages/internal-next/src/internalApi.ts b/packages/internal-next/src/internalApi.ts new file mode 100644 index 0000000000..3591bd7006 --- /dev/null +++ b/packages/internal-next/src/internalApi.ts @@ -0,0 +1,140 @@ +import type { ClocksState } from '@datadog/browser-core' +import { BufferedObservable, clocksNow } from '@datadog/browser-core' +import type { + AddDurationVitalOptions, + DurationVitalOptions, + DurationVitalReference, + RawRumEventCollectedData, + ViewCreatedEvent, +} from '@datadog/browser-rum-core' +import type { Logger, LogsMessage } from '@datadog/browser-logs' +import { SDK_VERSION } from './constants' + +const INTERNAL_API_NAMESPACE_KEY = Symbol.for('DD_INTERNAL') + +export interface InternalApi { + version: string + bus: BufferedObservable + notify(message: Message): void +} + +export interface MessageEnvelope { + clocks: ClocksState + message: Message +} + +export const enum MessageType { + CORE_SET_CONTEXT = 0, + CORE_SET_CONTEXT_PROPERTY = 1, + CORE_CLEAR_CONTEXT = 2, + RUNTIME_ERROR = 3, + RUM_ERROR = 4, + RUM_ACTION = 5, + RUM_ADD_DURATION_VITAL = 6, + RUM_START_DURATION_VITAL = 7, + RUM_STOP_DURATION_VITAL = 8, + RUM_RAW_EVENT_COLLECTED = 9, + RUM_VIEW_CREATED = 10, + LOGS_MESSAGE = 11, +} + +export const enum CoreContextType { + GLOBAL = 0, + USER = 1, + ACCOUNT = 2, +} + +export type Message = + | { + type: MessageType.CORE_SET_CONTEXT + context: CoreContextType + value: object + } + | { + type: MessageType.CORE_SET_CONTEXT_PROPERTY + context: CoreContextType + key: string + value: unknown + } + | { + type: MessageType.CORE_CLEAR_CONTEXT + context: CoreContextType + } + | { + type: MessageType.RUNTIME_ERROR + error: unknown + event?: ErrorEvent + } + | { + type: MessageType.RUM_ERROR + error: unknown + context: object | undefined + handlingStack: string + componentStack?: string + } + | { + type: MessageType.RUM_ACTION + name: string + context: object | undefined + handlingStack: string + } + | { + type: MessageType.RUM_ADD_DURATION_VITAL + name: string + options: AddDurationVitalOptions + } + | { + type: MessageType.RUM_START_DURATION_VITAL + name: string + ref: DurationVitalReference + options?: DurationVitalOptions + } + | { + type: MessageType.RUM_STOP_DURATION_VITAL + nameOrRef: string | DurationVitalReference + options?: DurationVitalOptions + } + | { + type: MessageType.RUM_RAW_EVENT_COLLECTED + data: RawRumEventCollectedData + } + | { + type: MessageType.RUM_VIEW_CREATED + event: ViewCreatedEvent + } + | { + type: MessageType.LOGS_MESSAGE + message: LogsMessage + logger: Logger + handlingStack?: string + } + +type GlobalWithInternalApiNamespace = typeof globalThis & { + [INTERNAL_API_NAMESPACE_KEY]?: { + // In the future, we can imagine having multiple instances of the internal API + default: InternalApi + } +} + +export function getInternalApi(): InternalApi { + // TODO: maybe enforce requesting a version + const g = globalThis as GlobalWithInternalApiNamespace + if (!g[INTERNAL_API_NAMESPACE_KEY]) { + g[INTERNAL_API_NAMESPACE_KEY] = { default: createInternalApi() } + } + return g[INTERNAL_API_NAMESPACE_KEY].default +} + +function createInternalApi(): InternalApi { + const bus = new BufferedObservable(1000) + return { + version: SDK_VERSION, + notify(message: Message) { + bus.notify({ + clocks: clocksNow(), + message, + }) + }, + bus, + } +} diff --git a/packages/internal-next/src/publicApi.ts b/packages/internal-next/src/publicApi.ts new file mode 100644 index 0000000000..d84e4a4ab4 --- /dev/null +++ b/packages/internal-next/src/publicApi.ts @@ -0,0 +1,12 @@ +type GlobalWithPublicApi = typeof globalThis & { + DATADOG: Record +} + +export function definePublicApiGlobal(publicApi: unknown, namespace?: string) { + const g = globalThis as GlobalWithPublicApi + let object: any = g.DATADOG || (g.DATADOG = {}) + if (namespace) { + object = object[namespace] || (object[namespace] = {}) + } + Object.assign(object, publicApi) +} diff --git a/packages/internal-next/src/types.ts b/packages/internal-next/src/types.ts new file mode 100644 index 0000000000..ed4c2b7d99 --- /dev/null +++ b/packages/internal-next/src/types.ts @@ -0,0 +1,30 @@ +import type { InitConfiguration, RelativeTime, SessionManager } from '@datadog/browser-core' +import type { LogsInitConfiguration } from '@datadog/browser-logs' +import type { RumInitConfiguration, SessionReplayState } from '@datadog/browser-rum-core' + +export type CoreInitializeConfiguration = Omit & { + workerUrl?: string + rum?: Omit> + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + profiling?: {} + logs?: Omit> +} + +export interface CoreSession { + id: string + sessionReplay: SessionReplayState + anonymousId?: string +} + +/** + * This SessionManager is used to mimick the RumSessionManager and LogsSessionManager. This is a + * temporary workaround while we are using legacy implementations that rely on the old + * SessionManager. + */ +export interface CoreSessionManager { + expire: SessionManager['expire'] + expireObservable: SessionManager['expireObservable'] + renewObservable: SessionManager['renewObservable'] + setForcedReplay: () => void + findTrackedSession: (startTime?: RelativeTime) => CoreSession | undefined +} diff --git a/packages/internal-next/typedoc.json b/packages/internal-next/typedoc.json new file mode 100644 index 0000000000..002b26a53c --- /dev/null +++ b/packages/internal-next/typedoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/entries/main.ts"] +} diff --git a/packages/logs-next/lazy/package.json b/packages/logs-next/lazy/package.json new file mode 100644 index 0000000000..4a716a76ba --- /dev/null +++ b/packages/logs-next/lazy/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "main": "../cjs/entries/lazy.js", + "module": "../esm/entries/lazy.js", + "types": "../cjs/entries/lazy.d.ts" +} diff --git a/packages/logs-next/package.json b/packages/logs-next/package.json new file mode 100644 index 0000000000..c6027ac34c --- /dev/null +++ b/packages/logs-next/package.json @@ -0,0 +1,16 @@ +{ + "name": "@datadog/browser-logs-next", + "private": true, + "main": "cjs/entries/main.js", + "module": "esm/entries/main.js", + "types": "cjs/entries/main.d.ts", + "scripts": { + "build": "node ../../scripts/build/build-package.ts --modules --bundle datadog-logs-next.js", + "build:bundle": "node ../../scripts/build/build-package.ts --bundle datadog-logs-next.js" + }, + "dependencies": { + "@datadog/browser-core": "6.25.0", + "@datadog/browser-internal-next": "workspace:*", + "@datadog/browser-logs": "6.25.0" + } +} diff --git a/packages/logs-next/src/entries/bundle.ts b/packages/logs-next/src/entries/bundle.ts new file mode 100644 index 0000000000..481daffd4a --- /dev/null +++ b/packages/logs-next/src/entries/bundle.ts @@ -0,0 +1,4 @@ +import { definePublicApiGlobal } from '@datadog/browser-internal-next' +import * as main from './main' + +definePublicApiGlobal(main, 'logs') diff --git a/packages/logs-next/src/entries/lazy.ts b/packages/logs-next/src/entries/lazy.ts new file mode 100644 index 0000000000..19b09f96df --- /dev/null +++ b/packages/logs-next/src/entries/lazy.ts @@ -0,0 +1,102 @@ +import { sendToExtension, startAccountContext, startGlobalContext, startUserContext } from '@datadog/browser-core' +import type { AbstractHooks, Batch, Context, ContextManager, EndpointBuilder, Telemetry } from '@datadog/browser-core' +import { bindContextManager, createBufferedDataFromMessageBus, MessageType } from '@datadog/browser-internal-next' +import type { CoreInitializeConfiguration, CoreSessionManager, InternalApi } from '@datadog/browser-internal-next' +import type { LogsEvent } from '@datadog/browser-logs' +import { + buildCommonContext, + LifeCycle, + LifeCycleEventType, + startConsoleCollection, + startLoggerCollection, + startLogsAssembly, + startNetworkErrorCollection, + startReportCollection, + startReportError, + startRuntimeErrorCollection, + startSessionContext, + validateAndBuildLogsConfiguration, +} from '@datadog/browser-logs/internal' + +export function initialize({ + coreInitializeConfiguration, + sessionManager, + createBatch, + hooks, + internalApi, + contexts, +}: { + coreInitializeConfiguration: CoreInitializeConfiguration + sessionManager: CoreSessionManager + createBatch: (endpoints: EndpointBuilder[]) => Batch + hooks: AbstractHooks + telemetry: Telemetry + internalApi: InternalApi + contexts: { + global: ContextManager + user: ContextManager + account: ContextManager + } +}) { + const configuration = validateAndBuildLogsConfiguration({ + ...coreInitializeConfiguration, + ...coreInitializeConfiguration.logs!, + }) + if (!configuration) { + return + } + const lifeCycle = new LifeCycle() + const cleanupTasks: Array<() => void> = [] + + lifeCycle.subscribe(LifeCycleEventType.LOG_COLLECTED, (log) => sendToExtension('logs', log)) + + const reportError = startReportError(lifeCycle) + + // Start user and account context first to allow overrides from global context + startSessionContext(hooks, configuration, sessionManager) + const accountContext = startAccountContext(hooks, configuration, 'logs') + const userContext = startUserContext(hooks, configuration, sessionManager, 'logs') + const globalContext = startGlobalContext(hooks, configuration, 'logs', false) + bindContextManager(contexts.global, globalContext) + bindContextManager(contexts.user, userContext) + bindContextManager(contexts.account, accountContext) + + // TODO: startRUMInternalContext(hooks) + + startNetworkErrorCollection(configuration, lifeCycle) + const bufferedDataObservable = createBufferedDataFromMessageBus(internalApi.bus) + startRuntimeErrorCollection(configuration, lifeCycle, bufferedDataObservable) + startConsoleCollection(configuration, lifeCycle) + startReportCollection(configuration, lifeCycle) + const { handleLog } = startLoggerCollection(lifeCycle) + internalApi.bus.subscribe(({ clocks, message }) => { + if (message.type === MessageType.LOGS_MESSAGE) { + handleLog( + message.message, + message.logger, + message.handlingStack, + undefined, // CommonContext? + clocks.timeStamp + ) + } + }) + + startLogsAssembly(configuration, lifeCycle, hooks, buildCommonContext, reportError) + + const endpoints = [configuration.logsEndpointBuilder] + if (configuration.replica) { + endpoints.push(configuration.replica.logsEndpointBuilder) + } + + const batch = createBatch(endpoints) + + lifeCycle.subscribe(LifeCycleEventType.LOG_COLLECTED, (serverLogsEvent: LogsEvent & Context) => { + batch.add(serverLogsEvent) + }) + + return { + stop: () => { + cleanupTasks.forEach((task) => task()) + }, + } +} diff --git a/packages/logs-next/src/entries/main.ts b/packages/logs-next/src/entries/main.ts new file mode 100644 index 0000000000..9150237421 --- /dev/null +++ b/packages/logs-next/src/entries/main.ts @@ -0,0 +1,20 @@ +import { getInternalApi, MessageType } from '@datadog/browser-internal-next' +import { Logger } from '@datadog/browser-logs/internal' +import type { LoggerConfiguration } from '@datadog/browser-logs' + +export function createLogger(name: string, conf?: LoggerConfiguration): Logger { + return new Logger( + (logsMessage, logger, handlingStack) => { + getInternalApi().notify({ + type: MessageType.LOGS_MESSAGE, + message: logsMessage, + logger, + handlingStack, + }) + }, + name, // TODO sanitize in lazy + conf?.handler, + conf?.level, + conf?.context + ) +} diff --git a/packages/logs-next/typedoc.json b/packages/logs-next/typedoc.json new file mode 100644 index 0000000000..002b26a53c --- /dev/null +++ b/packages/logs-next/typedoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/entries/main.ts"] +} diff --git a/packages/logs/internal/package.json b/packages/logs/internal/package.json new file mode 100644 index 0000000000..d382bb7e86 --- /dev/null +++ b/packages/logs/internal/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "main": "../cjs/entries/internal.js", + "module": "../esm/entries/internal.js", + "types": "../cjs/entries/internal.d.ts", + "sideEffects": false +} diff --git a/packages/logs/src/entries/internal.ts b/packages/logs/src/entries/internal.ts new file mode 100644 index 0000000000..17decee92b --- /dev/null +++ b/packages/logs/src/entries/internal.ts @@ -0,0 +1,12 @@ +export * from '../domain/lifeCycle' +export * from '../domain/reportError' +export * from '../domain/configuration' +export { startSessionContext } from '../domain/contexts/sessionContext' +export { startNetworkErrorCollection } from '../domain/networkError/networkErrorCollection' +export { startRuntimeErrorCollection } from '../domain/runtimeError/runtimeErrorCollection' +export { startConsoleCollection } from '../domain/console/consoleCollection' +export { startReportCollection } from '../domain/report/reportCollection' +export { startLoggerCollection } from '../domain/logger/loggerCollection' +export { startLogsAssembly } from '../domain/assembly' +export { buildCommonContext } from '../domain/contexts/commonContext' +export { Logger } from '../domain/logger' diff --git a/packages/profiling-next/lazy/package.json b/packages/profiling-next/lazy/package.json new file mode 100644 index 0000000000..4a716a76ba --- /dev/null +++ b/packages/profiling-next/lazy/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "main": "../cjs/entries/lazy.js", + "module": "../esm/entries/lazy.js", + "types": "../cjs/entries/lazy.d.ts" +} diff --git a/packages/profiling-next/package.json b/packages/profiling-next/package.json new file mode 100644 index 0000000000..53daccdca5 --- /dev/null +++ b/packages/profiling-next/package.json @@ -0,0 +1,13 @@ +{ + "name": "@datadog/browser-profiling-next", + "private": true, + "scripts": { + "build": "node ../../scripts/build/build-package.ts --modules" + }, + "dependencies": { + "@datadog/browser-core": "6.25.0", + "@datadog/browser-internal-next": "workspace:*", + "@datadog/browser-rum": "6.25.0", + "@datadog/browser-rum-core": "6.25.0" + } +} diff --git a/packages/profiling-next/src/entries/lazy.ts b/packages/profiling-next/src/entries/lazy.ts new file mode 100644 index 0000000000..dcccdfeea5 --- /dev/null +++ b/packages/profiling-next/src/entries/lazy.ts @@ -0,0 +1,93 @@ +import { addDuration, createValueHistory, noop, relativeToClocks, SESSION_TIME_OUT_DELAY } from '@datadog/browser-core' +import type { AbstractHooks, DeflateEncoderStreamId, Duration, Encoder, RelativeTime } from '@datadog/browser-core' +import { MessageType } from '@datadog/browser-internal-next' +import type { CoreInitializeConfiguration, CoreSessionManager, InternalApi } from '@datadog/browser-internal-next' +import type { LongTaskContext, LongTaskContexts, RumLongTaskEventDomainContext } from '@datadog/browser-rum-core' +import { LifeCycle, LifeCycleEventType, validateAndBuildRumConfiguration } from '@datadog/browser-rum-core' +import { createRumProfiler } from '@datadog/browser-rum/internal' +import { startProfilingContext } from '../profilingContext' + +const LONG_TASK_ID_HISTORY_TIME_OUT_DELAY = SESSION_TIME_OUT_DELAY + +export function initialize({ + coreInitializeConfiguration, + sessionManager, + internalApi, + hooks, + createEncoder, +}: { + coreInitializeConfiguration: CoreInitializeConfiguration + sessionManager: CoreSessionManager + hooks: AbstractHooks + internalApi: InternalApi + createEncoder: (streamId: DeflateEncoderStreamId) => Encoder +}) { + const configuration = validateAndBuildRumConfiguration({ + ...coreInitializeConfiguration, + ...coreInitializeConfiguration.rum!, + }) + if (!configuration) { + return + } + const lifeCycle = new LifeCycle() + + const profilingContextManager = startProfilingContext(hooks) + + let profilerWasStarted = false + + internalApi.bus.subscribe(({ message }) => { + switch (message.type) { + case MessageType.RUM_RAW_EVENT_COLLECTED: + lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, message.data) + break + case MessageType.RUM_VIEW_CREATED: + if (!profilerWasStarted) { + const profiler = createRumProfiler( + configuration, + lifeCycle, + sessionManager, + profilingContextManager, + createLongTaskContexts(internalApi), + createEncoder, + // TODO: change profiler so they don't require a full view history... + { + findView() { + return message.event + }, + stop: noop, + } + ) + profiler.start() + profilerWasStarted = true + } + lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, message.event) + break + } + }) +} + +function createLongTaskContexts(internalApi: InternalApi): LongTaskContexts { + // const history = createValueHistory({ + // expireDelay: LONG_TASK_ID_HISTORY_TIME_OUT_DELAY, + // }) + const longTasks: LongTaskContext[] = [] + + internalApi.bus.subscribe(({ message }) => { + if (message.type === MessageType.RUM_RAW_EVENT_COLLECTED && message.data.rawRumEvent.type === 'long_task') { + const { id } = message.data.rawRumEvent.long_task + const entry = (message.data.domainContext as RumLongTaskEventDomainContext).performanceEntry + const startClocks = relativeToClocks(entry.startTime as RelativeTime) + longTasks.push({ + id, + startClocks, + duration: entry.duration as Duration, + entryType: entry.entryType as any, + }) + // history.closeActive(addDuration(startClocks.relative, entry.duration as Duration)) + } + }) + + return { + findLongTasks: () => longTasks, + } +} diff --git a/packages/profiling-next/src/profilingContext.ts b/packages/profiling-next/src/profilingContext.ts new file mode 100644 index 0000000000..63310812dc --- /dev/null +++ b/packages/profiling-next/src/profilingContext.ts @@ -0,0 +1,36 @@ +import { HookNames, SKIPPED } from '@datadog/browser-core' +import type { Hooks, ProfilingInternalContextSchema } from '@datadog/browser-rum-core' +import { RumEventType } from '@datadog/browser-rum-core' + +export interface ProfilingContextManager { + set: (next: ProfilingInternalContextSchema) => void + get: () => ProfilingInternalContextSchema | undefined +} + +export const startProfilingContext = (hooks: Hooks): ProfilingContextManager => { + // Default status is `starting`. + let currentContext: ProfilingInternalContextSchema = { + status: 'starting', + } + + // Register the assemble hook to add the profiling context to the event attributes. + hooks.register(HookNames.Assemble, ({ eventType }) => { + if (eventType !== RumEventType.VIEW && eventType !== RumEventType.LONG_TASK) { + return SKIPPED + } + + return { + type: eventType, + _dd: { + profiling: currentContext, + }, + } + }) + + return { + get: () => currentContext, + set: (newContext: ProfilingInternalContextSchema) => { + currentContext = newContext + }, + } +} diff --git a/packages/profiling-next/typedoc.json b/packages/profiling-next/typedoc.json new file mode 100644 index 0000000000..ba67d96edb --- /dev/null +++ b/packages/profiling-next/typedoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": [] +} diff --git a/packages/rum-core/package.json b/packages/rum-core/package.json index 36afab23e7..310c645122 100644 --- a/packages/rum-core/package.json +++ b/packages/rum-core/package.json @@ -5,6 +5,7 @@ "main": "cjs/index.js", "module": "esm/index.js", "types": "cjs/index.d.ts", + "sideEffects": false, "scripts": { "build": "node ../../scripts/build/build-package.ts --modules" }, diff --git a/packages/rum-core/src/domain/vital/vitalCollection.ts b/packages/rum-core/src/domain/vital/vitalCollection.ts index 56d1af1842..48fa379a91 100644 --- a/packages/rum-core/src/domain/vital/vitalCollection.ts +++ b/packages/rum-core/src/domain/vital/vitalCollection.ts @@ -172,6 +172,7 @@ export function stopDurationVital( nameOrRef: string | DurationVitalReference, options: DurationVitalOptions = {} ) { + debugger const vitalStart = typeof nameOrRef === 'string' ? vitalsByName.get(nameOrRef) : vitalsByReference.get(nameOrRef) if (!vitalStart) { diff --git a/packages/rum-core/src/index.ts b/packages/rum-core/src/index.ts index 5f7b4283ea..84d2c0c008 100644 --- a/packages/rum-core/src/index.ts +++ b/packages/rum-core/src/index.ts @@ -23,9 +23,9 @@ export type { RumEventDomainContext, RumVitalEventDomainContext, } from './domainContext.types' -export type { ReplayStats, RawRumActionEvent, RawRumEvent } from './rawRumEvent.types' -export { ActionType, RumEventType, FrustrationType } from './rawRumEvent.types' -export { startRum } from './boot/startRum' +export type { ReplayStats, RawRumActionEvent, RawRumEvent, AssembledRumEvent } from './rawRumEvent.types' +export { ActionType, RumEventType, FrustrationType, VitalType } from './rawRumEvent.types' +export { startRum, startRumEventCollection } from './boot/startRum' export type { RawRumEventCollectedData } from './domain/lifeCycle' export { LifeCycle, LifeCycleEventType } from './domain/lifeCycle' export type { ViewCreatedEvent, ViewOptions } from './domain/view/trackViews' @@ -49,11 +49,13 @@ export type { FeatureFlagsForEvents, RemoteConfiguration, } from './domain/configuration' +export { validateAndBuildRumConfiguration } from './domain/configuration' export { DEFAULT_PROGRAMMATIC_ACTION_NAME_ATTRIBUTE } from './domain/action/actionNameConstants' export { STABLE_ATTRIBUTES } from './domain/getSelectorFromElement' export * from './browser/htmlDomUtils' export { getSessionReplayUrl } from './domain/getSessionReplayUrl' export { sanitizeIfLongDataUrl } from './domain/resource/resourceUtils' +export { createCustomVitalsState } from './domain/vital/vitalCollection' export * from './domain/privacy' export * from './domain/privacyConstants' export { SessionReplayState } from './domain/rumSessionManager' @@ -72,5 +74,6 @@ export type { Hooks, DefaultRumEventAttributes, DefaultTelemetryEventAttributes export { createHooks } from './domain/hooks' export { isSampled } from './domain/sampler/sampler' export type { TracingOption, PropagatorType } from './domain/tracing/tracer.types' +export { startCustomerDataTelemetry } from './domain/startCustomerDataTelemetry' export type { TransportPayload } from './transport/formDataTransport' export { createFormDataTransport } from './transport/formDataTransport' diff --git a/packages/rum-next/lazy/package.json b/packages/rum-next/lazy/package.json new file mode 100644 index 0000000000..4a716a76ba --- /dev/null +++ b/packages/rum-next/lazy/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "main": "../cjs/entries/lazy.js", + "module": "../esm/entries/lazy.js", + "types": "../cjs/entries/lazy.d.ts" +} diff --git a/packages/rum-next/package.json b/packages/rum-next/package.json new file mode 100644 index 0000000000..196af6cdf4 --- /dev/null +++ b/packages/rum-next/package.json @@ -0,0 +1,16 @@ +{ + "name": "@datadog/browser-rum-next", + "private": true, + "main": "cjs/entries/main.js", + "module": "esm/entries/main.js", + "types": "cjs/entries/main.d.ts", + "scripts": { + "build": "node ../../scripts/build/build-package.ts --modules --bundle datadog-rum-next.js", + "build:bundle": "node ../../scripts/build/build-package.ts --bundle datadog-rum-next.js" + }, + "dependencies": { + "@datadog/browser-core": "6.25.0", + "@datadog/browser-internal-next": "workspace:*", + "@datadog/browser-rum-core": "6.25.0" + } +} diff --git a/packages/rum-next/src/entries/bundle.ts b/packages/rum-next/src/entries/bundle.ts new file mode 100644 index 0000000000..94e0d2a46d --- /dev/null +++ b/packages/rum-next/src/entries/bundle.ts @@ -0,0 +1,4 @@ +import { definePublicApiGlobal } from '@datadog/browser-internal-next' +import * as main from './main' + +definePublicApiGlobal(main, 'rum') diff --git a/packages/rum-next/src/entries/lazy.ts b/packages/rum-next/src/entries/lazy.ts new file mode 100644 index 0000000000..aca6bab85d --- /dev/null +++ b/packages/rum-next/src/entries/lazy.ts @@ -0,0 +1,274 @@ +import type { + AbstractHooks, + Batch, + Context, + ContextManager, + Duration, + EndpointBuilder, + RawError, + Telemetry, + TimeStamp, + Observable, +} from '@datadog/browser-core' +import { + combine, + createPageMayExitObservable, + elapsed, + noop, + sanitize, + sendToExtension, + timeStampToClocks, +} from '@datadog/browser-core' +import { bindContextManager, createBufferedDataFromMessageBus, MessageType } from '@datadog/browser-internal-next' +import type { + CoreInitializeConfiguration, + CoreSessionManager, + InternalApi, + MessageEnvelope, +} from '@datadog/browser-internal-next' +import type { AssembledRumEvent, DurationVitalStart, RecorderApi } from '@datadog/browser-rum-core' +import { + LifeCycle, + LifeCycleEventType, + RumEventType, + startCustomerDataTelemetry, + validateAndBuildRumConfiguration, + createCustomVitalsState, + startRumEventCollection, + VitalType, + ActionType, +} from '@datadog/browser-rum-core' + +const recorderApi: RecorderApi = { + // TODO: RecorderApi + start: noop, + stop: noop, + isRecording: () => false, + onRumStart: noop, + getReplayStats: () => undefined, + getSessionReplayLink: () => undefined, +} + +export function initialize({ + coreInitializeConfiguration, + sessionManager, + createBatch, + hooks, + telemetry, + internalApi, + contexts, +}: { + coreInitializeConfiguration: CoreInitializeConfiguration + sessionManager: CoreSessionManager + createBatch: (endpoints: EndpointBuilder[]) => Batch + hooks: AbstractHooks + telemetry: Telemetry + internalApi: InternalApi + contexts: { + global: ContextManager + user: ContextManager + account: ContextManager + } +}) { + const configuration = validateAndBuildRumConfiguration({ + ...coreInitializeConfiguration, + ...coreInitializeConfiguration.rum!, + }) + if (!configuration) { + return + } + + const cleanupTasks: Array<() => void> = [] + const lifeCycle = new LifeCycle() + + sessionManager.expireObservable.subscribe(() => { + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) + }) + + sessionManager.renewObservable.subscribe(() => { + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + }) + + lifeCycle.subscribe(LifeCycleEventType.RUM_EVENT_COLLECTED, (event) => sendToExtension('rum', event)) + + lifeCycle.subscribe(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, (data) => + internalApi.notify({ + type: MessageType.RUM_RAW_EVENT_COLLECTED, + data, + }) + ) + lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, (event) => { + internalApi.notify({ + type: MessageType.RUM_VIEW_CREATED, + event, + }) + }) + + const reportError = (error: RawError) => { + lifeCycle.notify(LifeCycleEventType.RAW_ERROR_COLLECTED, { error }) + // TODO: expose telemetry functions + // monitor-until: forever, to keep an eye on the errors reported to customers + // addTelemetryDebug('Error reported to customer', { 'error.message': error.message }) + } + + // TODO: Is it fine to create another "page may exit" observable here? + const pageMayExitObservable = createPageMayExitObservable(configuration) + const pageMayExitSubscription = pageMayExitObservable.subscribe((event) => { + lifeCycle.notify(LifeCycleEventType.PAGE_MAY_EXIT, event) + }) + cleanupTasks.push(() => pageMayExitSubscription.unsubscribe()) + + const endpoints = [configuration.rumEndpointBuilder] + if (configuration.replica) { + endpoints.push(configuration.replica.rumEndpointBuilder) + } + const batch = createBatch(endpoints) + + lifeCycle.subscribe(LifeCycleEventType.RUM_EVENT_COLLECTED, (serverRumEvent: AssembledRumEvent) => { + if (serverRumEvent.type === RumEventType.VIEW) { + batch.upsert(serverRumEvent, serverRumEvent.view.id) + } else { + batch.add(serverRumEvent) + } + }) + startCustomerDataTelemetry(telemetry, lifeCycle, batch.flushController.flushObservable) + + // Convert internalApi message bus to bufferedDataObservable. + // TODO: in the future, use message bus directly in `startRumEventCollection` + const bufferedDataObservable = createBufferedDataFromMessageBus(internalApi.bus) + + const rumEvents = startRumEventCollection( + lifeCycle, + hooks, + configuration, + sessionManager, + recorderApi, + // TODO: initialViewOptions + undefined, + createCustomVitalsState(), // TODO: this is unused + bufferedDataObservable, + // TODO: sdkName + 'rum', + reportError + ) + cleanupTasks.push(rumEvents.stop) + + bindContextManager(contexts.global, rumEvents.globalContext) + bindContextManager(contexts.user, rumEvents.userContext) + bindContextManager(contexts.account, rumEvents.accountContext) + bindVitalCollection(internalApi.bus, rumEvents.addDurationVital) + bindErrorCollection(internalApi.bus, rumEvents.addError) + bindActionCollection(internalApi.bus, rumEvents.addAction) + + return { + stop: () => { + cleanupTasks.forEach((task) => task()) + }, + } +} + +function bindErrorCollection( + bus: Observable, + addError: ReturnType['addError'] +) { + bus.subscribe(({ clocks, message }) => { + switch (message.type) { + case MessageType.RUM_ERROR: { + addError({ + error: message.error, // Do not sanitize error here, it is needed unserialized by computeRawError() + handlingStack: message.handlingStack, + componentStack: message.componentStack, + context: sanitize(message.context) as Context, + startClocks: clocks, + }) + break + } + } + }) +} + +function bindActionCollection( + bus: Observable, + addAction: ReturnType['addAction'] +) { + bus.subscribe(({ clocks, message }) => { + switch (message.type) { + case MessageType.RUM_ACTION: { + addAction({ + name: sanitize(message.name)!, + context: sanitize(message.context) as Context, + startClocks: clocks, + type: ActionType.CUSTOM, + handlingStack: message.handlingStack, + }) + break + } + } + }) +} + +// TODO: in the future, handle this internally in `startVitalCollection` +function bindVitalCollection( + bus: Observable, + addDurationVital: ReturnType['addDurationVital'] +) { + const customVitalState = createCustomVitalsState() + bus.subscribe(({ clocks, message }) => { + switch (message.type) { + case MessageType.RUM_ADD_DURATION_VITAL: { + const options = message.options + addDurationVital({ + name: sanitize(message.name)!, + type: VitalType.DURATION, + startClocks: timeStampToClocks(options.startTime as TimeStamp), + duration: options.duration as Duration, + context: sanitize(options.context) as Context, + description: sanitize(options.description) as string | undefined, + }) + break + } + + case MessageType.RUM_START_DURATION_VITAL: { + const options = message.options + const name = sanitize(message.name)! + const vital = { + name, + startClocks: clocks, + context: sanitize(options && options.context) as Context, + description: sanitize(options && options.description) as string | undefined, + } + customVitalState.vitalsByName.set(name, vital) + customVitalState.vitalsByReference.set(message.ref, vital) + break + } + + case MessageType.RUM_STOP_DURATION_VITAL: { + const { options, nameOrRef } = message + + let vitalStart: DurationVitalStart | undefined + if (typeof nameOrRef === 'string') { + const sanitizedNameOrRef = sanitize(nameOrRef)! + vitalStart = customVitalState.vitalsByName.get(sanitizedNameOrRef) + customVitalState.vitalsByName.delete(sanitizedNameOrRef) + } else { + vitalStart = customVitalState.vitalsByReference.get(nameOrRef) + customVitalState.vitalsByReference.delete(nameOrRef) + } + + if (!vitalStart) { + return + } + + addDurationVital({ + name: vitalStart.name, + type: VitalType.DURATION, + startClocks: clocks, + duration: elapsed(vitalStart.startClocks.timeStamp, clocks.timeStamp), + context: combine(vitalStart.context, options?.context), + description: options?.description ?? vitalStart.description, + }) + break + } + } + }) +} diff --git a/packages/rum-next/src/entries/main.ts b/packages/rum-next/src/entries/main.ts new file mode 100644 index 0000000000..58f43a3283 --- /dev/null +++ b/packages/rum-next/src/entries/main.ts @@ -0,0 +1,50 @@ +import { createHandlingStack } from '@datadog/browser-core' +import { getInternalApi, MessageType } from '@datadog/browser-internal-next' +import type { AddDurationVitalOptions, DurationVitalOptions, DurationVitalReference } from '@datadog/browser-rum-core' + +export function addError(error: unknown, options?: { context?: object }) { + const handlingStack = createHandlingStack('error') + getInternalApi().notify({ + type: MessageType.RUM_ERROR, + error, + context: options?.context, + handlingStack, + }) +} + +export function addAction(name: string, context?: object) { + const handlingStack = createHandlingStack('error') + getInternalApi().notify({ + type: MessageType.RUM_ACTION, + name, + context, + handlingStack, + }) +} + +export function addDurationVital(name: string, options: AddDurationVitalOptions) { + getInternalApi().notify({ + type: MessageType.RUM_ADD_DURATION_VITAL, + name, + options, + }) +} + +export function startDurationVital(name: string, options?: DurationVitalOptions): DurationVitalReference { + const ref: DurationVitalReference = { __dd_vital_reference: true } + getInternalApi().notify({ + type: MessageType.RUM_START_DURATION_VITAL, + name, + ref, + options, + }) + return ref +} + +export function stopDurationVital(nameOrRef: string | DurationVitalReference, options?: DurationVitalOptions) { + getInternalApi().notify({ + type: MessageType.RUM_STOP_DURATION_VITAL, + nameOrRef, + options, + }) +} diff --git a/packages/rum-next/typedoc.json b/packages/rum-next/typedoc.json new file mode 100644 index 0000000000..002b26a53c --- /dev/null +++ b/packages/rum-next/typedoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/entries/main.ts"] +} diff --git a/packages/rum/internal/package.json b/packages/rum/internal/package.json index d5ac78d881..d382bb7e86 100644 --- a/packages/rum/internal/package.json +++ b/packages/rum/internal/package.json @@ -2,5 +2,6 @@ "private": true, "main": "../cjs/entries/internal.js", "module": "../esm/entries/internal.js", - "types": "../cjs/entries/internal.d.ts" + "types": "../cjs/entries/internal.d.ts", + "sideEffects": false } diff --git a/packages/rum/src/entries/internal.ts b/packages/rum/src/entries/internal.ts index a49154c92a..096e25e42a 100644 --- a/packages/rum/src/entries/internal.ts +++ b/packages/rum/src/entries/internal.ts @@ -16,3 +16,6 @@ export { export * from '../types' export { takeFullSnapshot, takeNodeSnapshot, serializeNode as serializeNodeWithId } from '../domain/record' + +export * from '../domain/profiling/profiler' +export * from '../domain/deflate' diff --git a/sandbox/next.html b/sandbox/next.html new file mode 100644 index 0000000000..5f643d9765 --- /dev/null +++ b/sandbox/next.html @@ -0,0 +1,33 @@ + + + + + Sandbox + + + + + + + + diff --git a/scripts/build/build-package.ts b/scripts/build/build-package.ts index 2bc8d47b7c..33d0cb0dc0 100644 --- a/scripts/build/build-package.ts +++ b/scripts/build/build-package.ts @@ -53,11 +53,12 @@ runMain(async () => { async function buildBundle({ filename, verbose }: { filename: string; verbose: boolean }) { await fs.rm('./bundle', { recursive: true, force: true }) + const entry = (await fileExists('./src/entries/bundle.ts')) ? './src/entries/bundle.ts' : './src/entries/main.ts' return new Promise((resolve, reject) => { webpack( webpackBase({ mode: 'production', - entry: './src/entries/main.ts', + entry, filename, }), (error, stats) => { @@ -83,6 +84,15 @@ async function buildBundle({ filename, verbose }: { filename: string; verbose: b } } +async function fileExists(path: string) { + try { + await fs.access(path) + return true + } catch { + return false + } +} + async function buildModules({ outDir, module, verbose }: { outDir: string; module: string; verbose: boolean }) { await fs.rm(outDir, { recursive: true, force: true }) diff --git a/scripts/dev-server.ts b/scripts/dev-server.ts index 18484735aa..c2c64f3a6f 100644 --- a/scripts/dev-server.ts +++ b/scripts/dev-server.ts @@ -1,3 +1,4 @@ +import { existsSync } from 'node:fs' import express from 'express' import middleware from 'webpack-dev-middleware' import HtmlWebpackPlugin from 'html-webpack-plugin' @@ -9,7 +10,7 @@ import { printLog, runMain } from './lib/executionUtils.ts' const sandboxPath = './sandbox' const port = 8080 -const PACKAGES_WITH_BUNDLE = ['rum', 'rum-slim', 'logs', 'flagging', 'worker'] +const PACKAGES_WITH_BUNDLE = ['rum', 'rum-slim', 'logs', 'flagging', 'worker', 'core-next', 'rum-next', 'logs-next'] runMain(() => { const app = express() @@ -29,7 +30,9 @@ function createStaticSandboxApp(): express.Application { webpack( webpackBase({ mode: 'development', - entry: `${packagePath}/src/entries/main.ts`, + entry: existsSync(`${packagePath}/src/entries/bundle.ts`) + ? `${packagePath}/src/entries/bundle.ts` + : `${packagePath}/src/entries/main.ts`, filename: packageName === 'worker' ? 'worker.js' : `datadog-${packageName}.js`, }) ) diff --git a/tsconfig.base.json b/tsconfig.base.json index c40d33dd99..d60891dd24 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -22,6 +22,7 @@ "@datadog/browser-flagging": ["./packages/flagging/src/entries/main"], "@datadog/browser-logs": ["./packages/logs/src/entries/main"], + "@datadog/browser-logs/internal": ["./packages/logs/src/entries/internal"], "@datadog/browser-rum-core": ["./packages/rum-core/src"], @@ -35,7 +36,21 @@ "@datadog/browser-rum-react/react-router-v6": ["./packages/rum-react/src/entries/reactRouterV6"], "@datadog/browser-rum-react/react-router-v7": ["./packages/rum-react/src/entries/reactRouterV7"], - "@datadog/browser-worker": ["./packages/worker/src/entries/main"] + "@datadog/browser-worker": ["./packages/worker/src/entries/main"], + + "@datadog/browser-flagging": ["./packages/flagging/src/entries/main"], + + "@datadog/browser-core-next": ["./packages/core-next/src/entries/main"], + + "@datadog/browser-rum-next": ["./packages/rum-next/src/entries/main"], + "@datadog/browser-rum-next/lazy": ["./packages/rum-next/src/entries/lazy"], + + "@datadog/browser-profiling-next/lazy": ["./packages/profiling-next/src/entries/lazy"], + + "@datadog/browser-logs-next": ["./packages/logs-next/src/entries/main"], + "@datadog/browser-logs-next/lazy": ["./packages/logs-next/src/entries/lazy"], + + "@datadog/browser-internal-next": ["./packages/internal-next/src/entries/main"] } } } diff --git a/webpack.base.ts b/webpack.base.ts index 3c1fead72c..697773b345 100644 --- a/webpack.base.ts +++ b/webpack.base.ts @@ -6,6 +6,19 @@ import { buildEnvKeys, getBuildEnvValue } from './scripts/lib/buildEnv.ts' const tsconfigPath = path.join(import.meta.dirname, 'tsconfig.webpack.json') +const packagesRoot = path.resolve(import.meta.dirname, 'packages') + +// Those modules are known to have side effects when evaluated +const pathsWithSideEffect = new Set([ + `${packagesRoot}/logs/src/entries/main.ts`, + `${packagesRoot}/flagging/src/entries/main.ts`, + `${packagesRoot}/rum/src/entries/main.ts`, + `${packagesRoot}/rum-slim/src/entries/main.ts`, + `${packagesRoot}/rum-next/src/entries/bundle.ts`, + `${packagesRoot}/core-next/src/entries/bundle.ts`, + `${packagesRoot}/core-next/src/entries/main.ts`, +]) + export default ({ entry, mode, @@ -28,13 +41,19 @@ export default ({ // can redirect requests for them reliably. `chunks/[name]-${filename}` : // Include a content hash in chunk names in production. - `chunks/[name]-[contenthash]-${filename}`, + 'chunks/[name]-[contenthash].js', + chunkLoading: 'import', + chunkFormat: 'module', path: path.resolve('./bundle'), }, target: ['web', 'es2018'], devtool: false, module: { rules: [ + { + test: (request) => !pathsWithSideEffect.has(request), + sideEffects: false, + }, { test: /\.(ts|tsx|js)$/, loader: 'ts-loader', @@ -60,9 +79,23 @@ export default ({ pako: 'pako/dist/pako.es5.js', }, }, - optimization: { chunkIds: 'named', + + splitChunks: { + chunks: 'async', + cacheGroups: { + defaultVendors: false, + default: false, + common: { + test: /core/, + name: 'common', + chunks: 'async', + minChunks: 3, // Every modules used at least 3 times are put in a common chunk + }, + }, + }, + minimizer: [ new TerserPlugin({ extractComments: false, @@ -89,7 +122,7 @@ export default ({ // the CDN (yet). { filename: '[file].map', - append: false, + // append: false, } ), createDefinePlugin({ keepBuildEnvVariables }), diff --git a/yarn.lock b/yarn.lock index b606b97810..43a27304bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -271,6 +271,20 @@ __metadata: languageName: node linkType: hard +"@datadog/browser-core-next@workspace:packages/core-next": + version: 0.0.0-use.local + resolution: "@datadog/browser-core-next@workspace:packages/core-next" + dependencies: + "@datadog/browser-core": "npm:6.25.0" + "@datadog/browser-internal-next": "workspace:*" + "@datadog/browser-logs-next": "workspace:*" + "@datadog/browser-profiling-next": "workspace:*" + "@datadog/browser-rum": "npm:6.25.0" + "@datadog/browser-rum-core": "npm:6.25.0" + "@datadog/browser-rum-next": "workspace:*" + languageName: unknown + linkType: soft + "@datadog/browser-core@npm:6.22.0": version: 6.22.0 resolution: "@datadog/browser-core@npm:6.22.0" @@ -301,7 +315,27 @@ __metadata: languageName: unknown linkType: soft -"@datadog/browser-logs@workspace:*, @datadog/browser-logs@workspace:packages/logs": +"@datadog/browser-internal-next@workspace:*, @datadog/browser-internal-next@workspace:packages/internal-next": + version: 0.0.0-use.local + resolution: "@datadog/browser-internal-next@workspace:packages/internal-next" + dependencies: + "@datadog/browser-core": "npm:6.25.0" + "@datadog/browser-logs": "npm:6.25.0" + "@datadog/browser-rum-core": "npm:6.25.0" + languageName: unknown + linkType: soft + +"@datadog/browser-logs-next@workspace:*, @datadog/browser-logs-next@workspace:packages/logs-next": + version: 0.0.0-use.local + resolution: "@datadog/browser-logs-next@workspace:packages/logs-next" + dependencies: + "@datadog/browser-core": "npm:6.25.0" + "@datadog/browser-internal-next": "workspace:*" + "@datadog/browser-logs": "npm:6.25.0" + languageName: unknown + linkType: soft + +"@datadog/browser-logs@npm:6.25.0, @datadog/browser-logs@workspace:*, @datadog/browser-logs@workspace:packages/logs": version: 0.0.0-use.local resolution: "@datadog/browser-logs@workspace:packages/logs" dependencies: @@ -314,6 +348,17 @@ __metadata: languageName: unknown linkType: soft +"@datadog/browser-profiling-next@workspace:*, @datadog/browser-profiling-next@workspace:packages/profiling-next": + version: 0.0.0-use.local + resolution: "@datadog/browser-profiling-next@workspace:packages/profiling-next" + dependencies: + "@datadog/browser-core": "npm:6.25.0" + "@datadog/browser-internal-next": "workspace:*" + "@datadog/browser-rum": "npm:6.25.0" + "@datadog/browser-rum-core": "npm:6.25.0" + languageName: unknown + linkType: soft + "@datadog/browser-rum-core@npm:6.25.0, @datadog/browser-rum-core@workspace:packages/rum-core": version: 0.0.0-use.local resolution: "@datadog/browser-rum-core@workspace:packages/rum-core" @@ -323,6 +368,16 @@ __metadata: languageName: unknown linkType: soft +"@datadog/browser-rum-next@workspace:*, @datadog/browser-rum-next@workspace:packages/rum-next": + version: 0.0.0-use.local + resolution: "@datadog/browser-rum-next@workspace:packages/rum-next" + dependencies: + "@datadog/browser-core": "npm:6.25.0" + "@datadog/browser-internal-next": "workspace:*" + "@datadog/browser-rum-core": "npm:6.25.0" + languageName: unknown + linkType: soft + "@datadog/browser-rum-react@workspace:packages/rum-react": version: 0.0.0-use.local resolution: "@datadog/browser-rum-react@workspace:packages/rum-react" @@ -368,7 +423,7 @@ __metadata: languageName: unknown linkType: soft -"@datadog/browser-rum@workspace:*, @datadog/browser-rum@workspace:packages/rum": +"@datadog/browser-rum@npm:6.25.0, @datadog/browser-rum@workspace:*, @datadog/browser-rum@workspace:packages/rum": version: 0.0.0-use.local resolution: "@datadog/browser-rum@workspace:packages/rum" dependencies: