Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into thibaut.gery/remove-f…
Browse files Browse the repository at this point in the history
…ocus-ff
  • Loading branch information
ThibautGeriz committed Sep 17, 2021
2 parents d726c18 + f61c866 commit 50d813e
Show file tree
Hide file tree
Showing 17 changed files with 220 additions and 231 deletions.
70 changes: 70 additions & 0 deletions packages/core/src/tools/observable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Observable } from '@datadog/browser-core'

describe('observable', () => {
let observable: Observable<void>
let subscriber: jasmine.Spy<jasmine.Func>

beforeEach(() => {
observable = new Observable()
subscriber = jasmine.createSpy('sub')
})

it('should allow to subscribe and be notified', () => {
observable.subscribe(subscriber)
expect(subscriber).not.toHaveBeenCalled()

observable.notify()
expect(subscriber).toHaveBeenCalledTimes(1)

observable.notify()
expect(subscriber).toHaveBeenCalledTimes(2)
})

it('should notify multiple clients', () => {
const otherSubscriber = jasmine.createSpy('sub2')
observable.subscribe(subscriber)
observable.subscribe(otherSubscriber)

observable.notify()

expect(subscriber).toHaveBeenCalled()
expect(otherSubscriber).toHaveBeenCalled()
})

it('should allow to unsubscribe', () => {
const subscription = observable.subscribe(subscriber)

subscription.unsubscribe()
observable.notify()

expect(subscriber).not.toHaveBeenCalled()
})

it('should execute onFirstSubscribe callback', () => {
const onFirstSubscribe = jasmine.createSpy('callback')
const otherSubscriber = jasmine.createSpy('sub2')
observable = new Observable(onFirstSubscribe)
expect(onFirstSubscribe).not.toHaveBeenCalled()

observable.subscribe(subscriber)
expect(onFirstSubscribe).toHaveBeenCalledTimes(1)

observable.subscribe(otherSubscriber)
expect(onFirstSubscribe).toHaveBeenCalledTimes(1)
})

it('should execute onLastUnsubscribe callback', () => {
const onLastUnsubscribe = jasmine.createSpy('callback')
const otherSubscriber = jasmine.createSpy('sub2')
observable = new Observable(() => onLastUnsubscribe)
const subscription = observable.subscribe(subscriber)
const otherSubscription = observable.subscribe(otherSubscriber)
expect(onLastUnsubscribe).not.toHaveBeenCalled()

subscription.unsubscribe()
expect(onLastUnsubscribe).not.toHaveBeenCalled()

otherSubscription.unsubscribe()
expect(onLastUnsubscribe).toHaveBeenCalled()
})
})
9 changes: 9 additions & 0 deletions packages/core/src/tools/observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@ export interface Subscription {

export class Observable<T> {
private observers: Array<(data: T) => void> = []
private onLastUnsubscribe?: () => void

constructor(private onFirstSubscribe?: () => (() => void) | void) {}

subscribe(f: (data: T) => void): Subscription {
if (!this.observers.length && this.onFirstSubscribe) {
this.onLastUnsubscribe = this.onFirstSubscribe() || undefined
}
this.observers.push(f)
return {
unsubscribe: () => {
this.observers = this.observers.filter((other) => f !== other)
if (!this.observers.length && this.onLastUnsubscribe) {
this.onLastUnsubscribe()
}
},
}
}
Expand Down
5 changes: 2 additions & 3 deletions packages/rum-core/src/boot/startRum.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { RelativeTime, Configuration } from '@datadog/browser-core'
import { RelativeTime, Configuration, Observable } from '@datadog/browser-core'
import { RumSession } from '@datadog/browser-rum-core'
import { createRumSessionMock, RumSessionMock } from '../../test/mockRumSession'
import { isIE } from '../../../core/test/specHelper'
import { noopRecorderApi, setup, TestSetupBuilder } from '../../test/specHelper'
import { DOMMutationObservable } from '../browser/domMutationObservable'
import { RumPerformanceNavigationTiming } from '../browser/performanceCollection'

import { LifeCycle, LifeCycleEventType } from '../domain/lifeCycle'
Expand All @@ -26,7 +25,7 @@ function startRum(
configuration: Configuration,
session: RumSession,
location: Location,
domMutationObservable: DOMMutationObservable
domMutationObservable: Observable<void>
) {
const { stop: rumEventCollectionStop, foregroundContexts } = startRumEventCollection(
applicationId,
Expand Down
52 changes: 9 additions & 43 deletions packages/rum-core/src/browser/domMutationObservable.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,23 @@
import { monitor, Subscription } from '@datadog/browser-core'
import { monitor, Observable } from '@datadog/browser-core'

export interface DOMMutationObservable {
subscribe(callback: () => void): Subscription
}

export function createDOMMutationObservable(): DOMMutationObservable {
let callbacks: Array<() => void> = []
export function createDOMMutationObservable() {
const MutationObserver = getMutationObserverConstructor()
const observer = MutationObserver ? new MutationObserver(monitor(notify)) : undefined

function notify() {
callbacks.forEach((callback) => callback())
}

function startDOMObservation() {
if (!observer) {
const observable: Observable<void> = new Observable<void>(() => {
if (!MutationObserver) {
return
}

const observer = new MutationObserver(monitor(() => observable.notify()))
observer.observe(document, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
})
}

function stopDOMObservation() {
if (!observer) {
return
}

observer.disconnect()
}
return () => observer.disconnect()
})

return {
subscribe: (callback) => {
if (!callbacks.length) {
startDOMObservation()
}

callbacks.push(callback)
return {
unsubscribe: () => {
callbacks = callbacks.filter((other) => callback !== other)

if (!callbacks.length) {
stopDOMObservation()
}
},
}
},
}
return observable
}

type MutationObserverConstructor = new (callback: MutationCallback) => MutationObserver
Expand All @@ -66,7 +32,7 @@ export function getMutationObserverConstructor(): MutationObserverConstructor |
let constructor: MutationObserverConstructor | undefined
const browserWindow: BrowserWindow = window

// Angular uses Zone.js to provide a context persisting accross async tasks. Zone.js replaces the
// Angular uses Zone.js to provide a context persisting across async tasks. Zone.js replaces the
// global MutationObserver constructor with a patched version to support the context propagation.
// There is an ongoing issue[1][2] with this setup when using a MutationObserver within a Angular
// component: on some occasions, the callback is being called in an infinite loop, causing the
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { combine, Configuration, toServerDuration, generateUUID } from '@datadog/browser-core'
import { combine, Configuration, toServerDuration, generateUUID, Observable } from '@datadog/browser-core'
import { ActionType, CommonContext, RumEventType, RawRumActionEvent } from '../../../rawRumEvent.types'
import { LifeCycle, LifeCycleEventType, RawRumEventCollectedData } from '../../lifeCycle'
import { DOMMutationObservable } from '../../../browser/domMutationObservable'
import { ForegroundContexts } from '../../foregroundContexts'
import { AutoAction, CustomAction, trackActions } from './trackActions'

export function startActionCollection(
lifeCycle: LifeCycle,
domMutationObservable: DOMMutationObservable,
domMutationObservable: Observable<void>,
configuration: Configuration,
foregroundContexts: ForegroundContexts
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { RumEvent } from '../../../../../rum/src'
import { setup, TestSetupBuilder } from '../../../../test/specHelper'
import { RumEventType, ActionType } from '../../../rawRumEvent.types'
import { LifeCycleEventType } from '../../lifeCycle'
import { PAGE_ACTIVITY_MAX_DURATION, PAGE_ACTIVITY_VALIDATION_DELAY } from '../../trackPageActivities'
import { AutoAction, trackActions } from './trackActions'
import { PAGE_ACTIVITY_VALIDATION_DELAY } from '../../trackPageActivities'
import { AutoAction, AUTO_ACTION_MAX_DURATION, trackActions } from './trackActions'

// Used to wait some time after the creation of a action
const BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY = PAGE_ACTIVITY_VALIDATION_DELAY * 0.8
// Used to wait some time but it doesn't matter how much.
const SOME_ARBITRARY_DELAY = 50
// A long delay used to wait after any action is finished.
const EXPIRE_DELAY = PAGE_ACTIVITY_MAX_DURATION * 10
const EXPIRE_DELAY = AUTO_ACTION_MAX_DURATION * 10

function eventsCollector<T>() {
const events: T[] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import {
clocksNow,
TimeStamp,
Configuration,
ONE_SECOND,
Observable,
} from '@datadog/browser-core'
import { ActionType } from '../../../rawRumEvent.types'
import { LifeCycle, LifeCycleEventType } from '../../lifeCycle'
import { EventCounts, trackEventCounts } from '../../trackEventCounts'
import { waitIdlePageActivity } from '../../trackPageActivities'
import { DOMMutationObservable } from '../../../browser/domMutationObservable'
import { getActionNameFromElement } from './getActionNameFromElement'

type AutoActionType = ActionType.CLICK
Expand Down Expand Up @@ -47,12 +48,15 @@ export interface AutoActionCreatedEvent {
startClocks: ClocksState
}

// Maximum duration for automatic actions
export const AUTO_ACTION_MAX_DURATION = 10 * ONE_SECOND

export function trackActions(
lifeCycle: LifeCycle,
domMutationObservable: DOMMutationObservable,
configuration: Configuration
domMutationObservable: Observable<void>,
{ actionNameAttribute }: Configuration
) {
const action = startActionManagement(lifeCycle, domMutationObservable, configuration)
const action = startActionManagement(lifeCycle, domMutationObservable)

// New views trigger the discard of the current pending Action
lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, () => {
Expand All @@ -66,7 +70,7 @@ export function trackActions(
if (!(event.target instanceof Element)) {
return
}
const name = getActionNameFromElement(event.target, configuration.actionNameAttribute)
const name = getActionNameFromElement(event.target, actionNameAttribute)
if (!name) {
return
}
Expand All @@ -84,11 +88,7 @@ export function trackActions(
}
}

function startActionManagement(
lifeCycle: LifeCycle,
domMutationObservable: DOMMutationObservable,
configuration: Configuration
) {
function startActionManagement(lifeCycle: LifeCycle, domMutationObservable: Observable<void>) {
let currentAction: PendingAutoAction | undefined
let currentIdlePageActivitySubscription: { stop: () => void }

Expand All @@ -104,15 +104,15 @@ function startActionManagement(
currentIdlePageActivitySubscription = waitIdlePageActivity(
lifeCycle,
domMutationObservable,
configuration,
(params) => {
if (params.hadActivity) {
pendingAutoAction.complete(params.endTime)
} else {
pendingAutoAction.discard()
}
currentAction = undefined
}
},
AUTO_ACTION_MAX_DURATION
)
},
discardCurrent: () => {
Expand Down
Loading

0 comments on commit 50d813e

Please sign in to comment.