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
11 changes: 11 additions & 0 deletions eslint-local-rules/disallowSideEffects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
])
Expand Down Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
20 changes: 20 additions & 0 deletions packages/core-next/package.json
Original file line number Diff line number Diff line change
@@ -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:*"
}
}
6 changes: 6 additions & 0 deletions packages/core-next/src/entries/bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { definePublicApiGlobal } from '@datadog/browser-internal-next'
import * as main from './main'

export * from './main'

definePublicApiGlobal(main)
76 changes: 76 additions & 0 deletions packages/core-next/src/entries/main.ts
Original file line number Diff line number Diff line change
@@ -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 })
}
44 changes: 44 additions & 0 deletions packages/core-next/src/initialize.ts
Original file line number Diff line number Diff line change
@@ -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)
})
}
156 changes: 156 additions & 0 deletions packages/core-next/src/lazy.ts
Original file line number Diff line number Diff line change
@@ -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<string>(
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' }),
}
}
21 changes: 21 additions & 0 deletions packages/core-next/src/lazyCompression.ts
Original file line number Diff line number Diff line change
@@ -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),
}
}
4 changes: 4 additions & 0 deletions packages/core-next/typedoc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://typedoc.org/schema.json",
"entryPoints": ["src/entries/main.ts"]
}
3 changes: 2 additions & 1 deletion packages/core/src/browser/addEventListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const enum DOM_EVENT {
SECURITY_POLICY_VIOLATION = 'securitypolicyviolation',
SELECTION_CHANGE = 'selectionchange',
STORAGE = 'storage',
ERROR = 'error',
}

interface AddEventListenerOptions {
Expand All @@ -47,7 +48,7 @@ interface AddEventListenerOptions {
passive?: boolean
}

type EventMapFor<T> = T extends Window
type EventMapFor<T> = T extends Window | typeof globalThis
? WindowEventMap & {
// TS 4.9.5 does not support `freeze` and `resume` events yet
freeze: Event
Expand Down
Loading