Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Change view logic to emit LifeCycle events #366

Merged
merged 14 commits into from
Apr 21, 2020
Merged
Show file tree
Hide file tree
Changes from 3 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
12 changes: 4 additions & 8 deletions packages/core/src/transport.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import lodashMerge from 'lodash.merge'

import { monitor } from './internalMonitoring'
import { Context, DOM_EVENT, jsonStringify, objectValues } from './utils'
import { Context, DOM_EVENT, jsonStringify, noop, objectValues } from './utils'

/**
* Use POST request without content type to:
Expand Down Expand Up @@ -45,7 +45,8 @@ export class Batch<T> {
private bytesLimit: number,
private maxMessageSize: number,
private flushTimeout: number,
private contextProvider: () => Context
private contextProvider: () => Context,
private willUnloadCallback: () => void = noop
) {
this.flushOnVisibilityHidden()
this.flushPeriodically()
Expand Down Expand Up @@ -160,12 +161,7 @@ export class Batch<T> {
* register first to be sure to be called before flush on beforeunload
* caveat: unload can still be canceled by another listener
*/
window.addEventListener(
DOM_EVENT.BEFORE_UNLOAD,
monitor(() => {
this.beforeFlushOnUnloadHandlers.forEach((handler) => handler())
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
})
)
window.addEventListener(DOM_EVENT.BEFORE_UNLOAD, monitor(this.willUnloadCallback))
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved

/**
* Only event that guarantee to fire on mobile devices when the page transitions to background state
Expand Down
8 changes: 7 additions & 1 deletion packages/rum/src/lifeCycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ export enum LifeCycleEventType {
USER_ACTION_COLLECTED,
REQUEST_STARTED,
REQUEST_COMPLETED,
SESSION_WILL_RENEW,
SESSION_RENEWED,
RESOURCE_ADDED_TO_BATCH,
DOM_MUTATED,
WILL_UNLOAD,
}

export interface Subscription {
Expand All @@ -26,9 +28,11 @@ export class LifeCycle {
notify(eventType: LifeCycleEventType.USER_ACTION_COLLECTED, data: UserAction): void
notify(
eventType:
| LifeCycleEventType.SESSION_WILL_RENEW
| LifeCycleEventType.SESSION_RENEWED
| LifeCycleEventType.RESOURCE_ADDED_TO_BATCH
| LifeCycleEventType.DOM_MUTATED
| LifeCycleEventType.WILL_UNLOAD
): void
notify(eventType: LifeCycleEventType, data?: any) {
const eventCallbacks = this.callbacks[eventType]
Expand All @@ -50,9 +54,11 @@ export class LifeCycle {
subscribe(eventType: LifeCycleEventType.USER_ACTION_COLLECTED, callback: (data: UserAction) => void): Subscription
subscribe(
eventType:
| LifeCycleEventType.SESSION_WILL_RENEW
| LifeCycleEventType.SESSION_RENEWED
| LifeCycleEventType.RESOURCE_ADDED_TO_BATCH
| LifeCycleEventType.DOM_MUTATED,
| LifeCycleEventType.DOM_MUTATED
| LifeCycleEventType.WILL_UNLOAD,
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
callback: () => void
): Subscription
subscribe(eventType: LifeCycleEventType, callback: (data?: any) => void) {
Expand Down
20 changes: 11 additions & 9 deletions packages/rum/src/rum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
import { InternalContext, RumGlobal } from './rum.entry'
import { RumSession } from './rumSession'
import { getUserActionReference, UserActionMeasures, UserActionReference, UserActionType } from './userActionCollection'
import { trackView, viewContext, ViewMeasures } from './viewTracker'
import { trackView, viewContext, ViewMeasures } from './viewCollection'

export interface PerformancePaintTiming extends PerformanceEntry {
entryType: 'paint'
Expand Down Expand Up @@ -155,7 +155,7 @@ export function startRum(
lodashMerge(
{
application_id: applicationId,
session_id: viewContext.sessionId,
session_id: session.getId(),
view: {
id: viewContext.id,
},
Expand All @@ -175,17 +175,18 @@ export function startRum(
session: {
type: sessionTpe,
},
sessionId: viewContext.sessionId,
sessionId: session.getId(),
view: {
id: viewContext.id,
referrer: document.referrer,
url: viewContext.location.href,
},
}),
() => globalContext
() => globalContext,
() => lifeCycle.notify(LifeCycleEventType.WILL_UNLOAD)
)

trackView(window.location, lifeCycle, session, batch.upsertRumEvent, batch.beforeFlushOnUnload)
trackView(window.location, lifeCycle, batch.upsertRumEvent)
trackErrors(lifeCycle, batch.addRumEvent)
trackRequests(configuration, lifeCycle, session, batch.addRumEvent)
trackPerformanceTiming(configuration, lifeCycle, batch.addRumEvent)
Expand All @@ -203,7 +204,7 @@ export function startRum(
(): InternalContext => {
return {
application_id: applicationId,
session_id: viewContext.sessionId,
session_id: session.getId(),
user_action: getUserActionReference(),
view: {
id: viewContext.id,
Expand All @@ -221,23 +222,24 @@ function startRumBatch(
configuration: Configuration,
session: RumSession,
rumContextProvider: () => Context,
globalContextProvider: () => Context
globalContextProvider: () => Context,
willUnloadCallback: () => void
) {
const batch = new Batch<Context>(
new HttpRequest(configuration.rumEndpoint, configuration.batchBytesLimit, true),
configuration.maxBatchSize,
configuration.batchBytesLimit,
configuration.maxMessageSize,
configuration.flushTimeout,
() => lodashMerge(withSnakeCaseKeys(rumContextProvider()), globalContextProvider())
() => lodashMerge(withSnakeCaseKeys(rumContextProvider()), globalContextProvider()),
willUnloadCallback
)
return {
addRumEvent: (event: RumEvent, context?: Context) => {
if (session.isTracked()) {
batch.add({ ...context, ...withSnakeCaseKeys((event as unknown) as Context) })
}
},
beforeFlushOnUnload: (handler: () => void) => batch.beforeFlushOnUnload(handler),
upsertRumEvent: (event: RumEvent, key: string) => {
if (session.isTracked()) {
batch.upsert(withSnakeCaseKeys((event as unknown) as Context), key)
Expand Down
46 changes: 40 additions & 6 deletions packages/rum/src/rumSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,53 @@ export enum RumSessionType {
TRACKED_WITHOUT_RESOURCES = '2',
}

class StoredSession {
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
private id: string | undefined
private type: RumSessionType | undefined

constructor(private isAlive: () => boolean) {}

store(id: string | undefined, type: RumSessionType | undefined) {
this.id = id
this.type = type
}

getId() {
this.makeSureSessionIsAlive()
return this.id
}

isTracked() {
this.makeSureSessionIsAlive()
return isTracked(this.type)
}

isTrackedWithResource() {
this.makeSureSessionIsAlive()
return this.type === RumSessionType.TRACKED_WITH_RESOURCES
}

private makeSureSessionIsAlive() {
if (!this.isAlive()) {
this.id = undefined
this.type = undefined
}
}
}

export function startRumSession(configuration: Configuration, lifeCycle: LifeCycle): RumSession {
const session = startSessionManagement(RUM_SESSION_KEY, (rawType) => computeSessionState(configuration, rawType))
const storedSession = new StoredSession(() => session.getId() !== undefined)

storedSession.store(session.getId(), session.getType())

session.renewObservable.subscribe(() => {
lifeCycle.notify(LifeCycleEventType.SESSION_WILL_RENEW)
storedSession.store(session.getId(), session.getType())
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
})
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved

return {
getId: session.getId,
isTracked: () => session.getId() !== undefined && isTracked(session.getType()),
isTrackedWithResource: () =>
session.getId() !== undefined && session.getType() === RumSessionType.TRACKED_WITH_RESOURCES,
}
return storedSession
}

function computeSessionState(configuration: Configuration, rawSessionType?: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { DOM_EVENT, generateUUID, getTimestamp, monitor, msToNs, throttle } from

import { LifeCycle, LifeCycleEventType } from './lifeCycle'
import { PerformancePaintTiming, RumEvent, RumEventCategory } from './rum'
import { RumSession } from './rumSession'
import { trackEventCounts } from './trackEventCounts'

export interface ViewMeasures {
Expand All @@ -20,7 +19,6 @@ export interface ViewMeasures {
interface ViewContext {
id: string
location: Location
sessionId: string | undefined
}

export let viewContext: ViewContext
Expand All @@ -33,9 +31,7 @@ let viewMeasures: ViewMeasures
export function trackView(
location: Location,
lifeCycle: LifeCycle,
session: RumSession,
upsertRumEvent: (event: RumEvent, key: string) => void,
beforeFlushOnUnload: (handler: () => void) => void
upsertRumEvent: (event: RumEvent, key: string) => void
) {
const scheduleViewUpdate = throttle(monitor(() => updateView(upsertRumEvent)), THROTTLE_VIEW_UPDATE_PERIOD, {
leading: false,
Expand All @@ -45,25 +41,23 @@ export function trackView(
viewMeasures = { ...viewMeasures, ...eventCounts }
scheduleViewUpdate()
})
newView(location, session, resetEventCounts, upsertRumEvent)
trackHistory(location, session, resetEventCounts, upsertRumEvent)
newView(location, resetEventCounts, upsertRumEvent)
trackHistory(location, resetEventCounts, upsertRumEvent)
trackTimings(lifeCycle, scheduleViewUpdate)
trackRenewSession(location, lifeCycle, session, resetEventCounts, upsertRumEvent)
trackRenewSession(location, lifeCycle, resetEventCounts, upsertRumEvent)

beforeFlushOnUnload(() => updateView(upsertRumEvent))
lifeCycle.subscribe(LifeCycleEventType.WILL_UNLOAD, () => updateView(upsertRumEvent))
}

function newView(
location: Location,
session: RumSession,
resetEventCounts: () => void,
upsertRumEvent: (event: RumEvent, key: string) => void
) {
startOrigin = !viewContext ? 0 : performance.now()
viewContext = {
id: generateUUID(),
location: { ...location },
sessionId: session.getId(),
}
documentVersion = 1
viewMeasures = {
Expand Down Expand Up @@ -102,37 +96,35 @@ function upsertViewEvent(upsertRumEvent: (event: RumEvent, key: string) => void)

function trackHistory(
location: Location,
session: RumSession,
resetEventCounts: () => void,
upsertRumEvent: (event: RumEvent, key: string) => void
) {
const originalPushState = history.pushState
history.pushState = monitor(function(this: History['pushState']) {
originalPushState.apply(this, arguments as any)
onUrlChange(location, session, resetEventCounts, upsertRumEvent)
onUrlChange(location, resetEventCounts, upsertRumEvent)
})
const originalReplaceState = history.replaceState
history.replaceState = monitor(function(this: History['replaceState']) {
originalReplaceState.apply(this, arguments as any)
onUrlChange(location, session, resetEventCounts, upsertRumEvent)
onUrlChange(location, resetEventCounts, upsertRumEvent)
})
window.addEventListener(
DOM_EVENT.POP_STATE,
monitor(() => {
onUrlChange(location, session, resetEventCounts, upsertRumEvent)
onUrlChange(location, resetEventCounts, upsertRumEvent)
})
)
}

function onUrlChange(
location: Location,
session: RumSession,
resetEventCounts: () => void,
upsertRumEvent: (event: RumEvent, key: string) => void
) {
if (areDifferentViews(viewContext.location, location)) {
updateView(upsertRumEvent)
newView(location, session, resetEventCounts, upsertRumEvent)
newView(location, resetEventCounts, upsertRumEvent)
}
}

Expand Down Expand Up @@ -166,12 +158,14 @@ function trackTimings(lifeCycle: LifeCycle, scheduleViewUpdate: () => void) {
function trackRenewSession(
location: Location,
lifeCycle: LifeCycle,
session: RumSession,
resetEventCounts: () => void,
upsertRumEvent: (event: RumEvent, key: string) => void
) {
lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, () => {
lifeCycle.subscribe(LifeCycleEventType.SESSION_WILL_RENEW, () => {
updateView(upsertRumEvent)
newView(location, session, resetEventCounts, upsertRumEvent)
})

lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, () => {
newView(location, resetEventCounts, upsertRumEvent)
})
}
1 change: 1 addition & 0 deletions packages/rum/test/rum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ describe('rum session', () => {
expect(initialRequests[0].session_id).toEqual('42')

server.requests = []
lifeCycle.notify(LifeCycleEventType.SESSION_WILL_RENEW)
sessionId = '43'
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)

Expand Down
51 changes: 47 additions & 4 deletions packages/rum/test/rumSession.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,62 @@ describe('rum session', () => {
expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${RUM_SESSION_KEY}=${RumSessionType.NOT_TRACKED}`)
})

it('should renew on activity after expiration', () => {
startRumSession(configuration as Configuration, lifeCycle)

function renewSession() {
setCookie(SESSION_COOKIE_NAME, '', DURATION)
expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined()
expect(renewSessionSpy).not.toHaveBeenCalled()
jasmine.clock().tick(COOKIE_ACCESS_DELAY)

setupDraws({ tracked: true, trackedWithResources: true })
document.dispatchEvent(new CustomEvent('click'))
}

it('should renew on activity after expiration', () => {
startRumSession(configuration as Configuration, lifeCycle)

expect(renewSessionSpy).not.toHaveBeenCalled()

renewSession()

expect(renewSessionSpy).toHaveBeenCalled()
expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${RUM_SESSION_KEY}=${RumSessionType.TRACKED_WITH_RESOURCES}`)
expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/id=[a-f0-9-]/)
})

it('should return undefined values when the session is expired', () => {
setCookie(SESSION_COOKIE_NAME, 'id=abcdef&rum=1', DURATION)

const session = startRumSession(configuration as Configuration, lifeCycle)

setCookie(SESSION_COOKIE_NAME, '', DURATION)
jasmine.clock().tick(COOKIE_ACCESS_DELAY)

expect(session.getId()).toBe(undefined)
expect(session.isTracked()).toBe(false)
expect(session.isTrackedWithResource()).toBe(false)
})

it('should return the ending session id when notifying the WILL_RENEW life-cycle', () => {
setCookie(SESSION_COOKIE_NAME, 'id=abcdef&rum=1', DURATION)

const session = startRumSession(configuration as Configuration, lifeCycle)
const idBeforeRenew = session.getId()
let idDuringWillRenewEvent
let idDuringRenewedEvent

lifeCycle.subscribe(LifeCycleEventType.SESSION_WILL_RENEW, () => {
idDuringWillRenewEvent = session.getId()
})

lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, () => {
idDuringRenewedEvent = session.getId()
})

renewSession()

expect(idBeforeRenew).toEqual(jasmine.any(String))
expect(idDuringWillRenewEvent).toEqual(jasmine.any(String))
expect(idDuringRenewedEvent).toEqual(jasmine.any(String))
expect(idBeforeRenew).toBe(idDuringWillRenewEvent)
expect(idBeforeRenew).not.toBe(idDuringRenewedEvent)
})
})
Loading